[
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [3.3.0] - 2026-04-01\n\n### Added\n\n- Display basic save info when a save is loaded (region, version, partial inquiry code)\n\n- Prompt to ask if you want to change game data repo to gitlab if battlecatsmodding.org is not loading\n\n### Fixed\n\n- JP 15.3.0 save parsing\n\n- Version checking for version numbers >= 10\n\n- Talent orb effect colours being broken on some terminals\n\n- Maybe fixed cat shrine not appearing when using the feature to make it appear\n\n- Some issues when reading/writing invalid cat talents\n\n- Editor crashing when inputing an option too high if disable maxes is enabled\n\n## [3.2.2] - 2025-12-29\n\n### Added\n\n- Options within the cat shrine feature to make it appear/disappear in-game\n\n### Changed\n\n- Reworked how clearing maps/stages works for non story chapters - setting map progress and clear\ncount are now done separately.\n\n### Fixed\n\n- Fixed a few more issues with game data downloading\n\n- Fixed some issues where disabling max values didn't work for some features\n\n- Fixed unknwon map names sometimes showing as blank rather than \"Unknown Map Name\"\n\n- Improved speed of getting map names for most map clearing features\n\n- Special skill upgrading crashing if you disable max values and you enter an upgrade value which is too large\n\n- Game data repo config value URL is validated when trying to change it\n\n## [3.2.1] - 2025-11-23\n\n### Added\n\n- Feature to add endless battle items\n\n### Fixed\n\n- Fixed game data downloading when upgrading from a previous editor version\n\n- Fixed disable max values not working in some situations\n\n- Fixed an issue where maps not present in the save file could be selected, causing an error\n\n## [3.2.0] - 2025-11-21\n\n### Added\n\n- Feature to edit the cat storage\n\n- Feature to reset the golden cat cpu uses\n\n- Feature to clear enigma stages\n\n- Feature to clear catclaw championships\n\n- Feature to reset cat scratcher and wildcat slots\n\n- Cats of the Cosmos Chapter 3 outbreaks\n\n- Features to push to root storage and rerun game\n\n- Option to select cats by the game version they were released in\n\n### Fixed\n\n- Fixed issue of not being able to add enigma stages if they have already been cleared\n\n- Fixed issue on some platforms where it couldn't find JSONDecodeError\n\n- Fixed a few text and input issues\n\n- Fixed issue where editing event tickets could cause your inquiry code to change\n\n### Changed\n\n- Game data repo is now at <https://git.battlecatsmodding.org/fieryhenry/BCData>\n\n- Game data is now downloaded and saved all at once, rather than file by file which should speed up some features\n\n- When running the Update External Content feature, it asks you if you want to clear the current game data\n\n## [3.1.0] - 2025-09-03\n\n### Added\n\n- Added a way to choose a gacha banner by id rather than by name\n\n- Feature to clear catamin stages and set the clear times to whatever you want\n\n- When selecting cats you can now filter down your selection, e.g Select current rare cats from\n  this specific gacha banner\n\n- Better error message if your device is not rooted when trying to pull from root storage\n\n- Better error message when failing to create a config file\n\n\n### Changed\n\n- Changed default game data repo from <https://github.com/fieryhenry/BCData> to <https://git.fyhenry.uk/BCData>\n\n- Changed a few other urls to point to the Codeberg repo rather than the GitHub repo\n\n- Increased speed of downloading all cat names\n\n- Made a few more option selections work if you enter the text of the option rather than the\n  number associated with it\n\n\n### Fixed\n\n- Fixed talent parsing issue for some save files\n\n- Fixed gacha banner selection using old banners rather than newer ones for banners which have the\n  same name\n\n- Disable maxes config option now works for upgrading cats\n\n- Fixed regression where editing scheme items raised an exception\n\n- Increased default timeout for requests and made some requests have no timeout to fix issues with\n  slow internet connection\n\n- Issue where an exception would be raised if the editor couldn't auto-detect your country code\n  when downloading save data\n\n\n## [3.0.1] - 2025-08-04\n\n### Fixed\n\n- Installing the editor on certain platforms due to a dependency issue\n\n## [3.0.0] - 2025-08-04\n\nThis is a full re-write of the editor, so many things were added, changed and fixed, and I didn't\nreally document the changes that well, so here's a summary:\n\n### Added\n\n- Game Data for es, it, de, th, fr locales and a way to change what repo to use for game data\n\n- Better color and localization support\n\n- Better adb usage\n\n- Waydroid support\n\n- More config options\n\n- Lucky tickets\n\n- Treasure Chests\n\n- Support for new talent orbs\n\n- Ultra Form Support\n\n- Better save backups\n\n_ More support for older game versions\n\n- Labyrinth medals\n\n- Many more things\n\n### Changed\n\n- Improved the wording on a few features\n\n- Cats will be auto-unlocked by default when upgrading / true forming, etc\n\n- Many more things\n\n## [2.7.2.3] - 2023-06-19\n\n### Fixed\n\n- New version of colored crashing the editor by forcing the editor to use the\nold version (new colored version renamed stuff and also raises an exception when\nnot using a specific set of colors)\n\n- Max value for equip slots being too high, for some reason ponos has allocated\nspace for 18 equip slots but has only allocated space for 17 slot names\n\n## [2.7.2.2] - 2023-06-08\n\n### Fixed\n\n- The editor crashing when editing meow medals or event stages\n\n## [2.7.2.1] - 2023-05-28\n\n### Fixed\n\n- The editor crashing if user info not found\n\n## [2.7.2] - 2023-05-28\n\n### Added\n\n- Ultra Talent Support\n\n- 12.2.0 cannon support\n\n### Changed\n\n- Improved item tracking and user info tracking so your inquiry code shouldn't\nchange as much\n\n### Fixed\n\n- Issues with the max values for some multi items\n\n- Disable maxes config option not being checked\n\n- Gold pass not being able to be removed\n\n## [2.7.1] - 2023-03-22\n\n### Added\n\n- A feature to convert save versions e.g en to jp - might give issues and only\nworks if both apps are the same version\n\n### Changed\n\n- Base material names are no longer hardcoded and so jp base material names exist\nnow\n\n- New cats no longer cause the cat capsule machine still thinking the cat is new\n\n- Talent orbs editing works better now + aku orbs\n\n### Fixed\n\n- Things like treasures and gold pass id crashing the editor when entering too\nlarge of a number\n\n- Jp 12.2.0 save parsing\n\n- The first mission not showing up in mission clearing\n\n- Gatya seed not being able to set above the 32 signed int limit\n\n- Outbreaks crashing\n\n## [2.7.0] - 2023-01-08\n\n### Added\n\n- Features to clear legend quest, behemoth culling stages, and collab gauntlets\n\n- Feature to get scheme item rewards (e.g go go pogo cat mission rewards)\n\n- More support for rooted android devices (pull and push directly to root\nfolder + re-run game)\n\n- The ability to remove talents\n\n- The ability to select / download a new save without having to restart the editor\n\n### Changed\n\n- Catseye editing will now use the game data for names - means i don't need to\nupdate the whole editor to put another catseye type in\n\n- When uploading the managed items, a save key is added (idk if this changes\nanything / reduces bans but newer game versions do this)\n\n- The editor will never ask if you want to exit, to exit enter the option to exit\nor do `ctrl+c`\n\n- Renamed feature `Create a new account` to `Generate a new inquiry code and token`\nto better reflect what it does\n\n### Fixed\n\n- Cat name selection for jp\n\n- Evolve cats and upgrade cats crashing if game data is outdated\n\n- Main story crashing and chapter names being offset sometimes\n\n- Dojo score not being able to be edited if you haven't been to the dojo yet\n\n- Max value for some items being an unsigned int even though the game reads\nsigned ints\n\n- Outbreak clearing not setting all stages\n\n- Jp timed score rewards being parsed and serialized incorrectly leading to\nincorrect timed scores being edited in\n\n### Removed\n\n- The `pick` module due to issues with python 3.11\n\n## [2.6.0] - 2022-10-24\n\n### Added\n\n- Editor support for android. Using termux you can now run and install the editor\n\n- On crash, the editor will ask if you want to save your changes and upload\nmanaged item changes to the servers\n\n- A way to remove meow medals\n\n- A feature to play the CotC 3 filibuster stage again\n\n- A way to remove outbreaks\n\n- A feature to unlock the aku realm\n\n### Changed\n\n- When upgrading cats, if you upgrade past the normal max for that cat then the\nlevel cap of the cat will also increase / decrease to match. (E.g if you upgrade\na cat to level 35 using the editor, then use a catseye in game then it will\nunlock level 36 instead of level 31)\n\n- How selecting stages to clear works. Instead of selecting stage ids you enter\na stage to complete the progress to (e.g entering 5 clears the first 5 stages,\nand entering 48 clears them all and then if you then enter 5 again it will clear\nthe level progress for the levels 6-48)\n\n### Fixed\n\n- Crash if using an older game version and getting cats by rarity / gatya id\n\n- Talents crashing\n\n- CotC 2 and 3 appearing in the outbreaks feature when they don't have outbreaks\n\n- The editor crashing if you don't have an internet connection\n\n## [2.5.0] - 2022-10-14\n\n### Added\n\n- A feature to fix time related issues (HGT, no energy recovery, etc)\n\n### Changed\n\n- Features that fix things (fix time related issues, fix gamatoto crashing the\ngame, fix equip menu not unlocked, etc) have been moved / copied to their own\ncategory called `Fixes`\n\n### Fixed\n\n- Having a very high playtime not allowing you to transfer\n\n- Having corrupted cat unlock flags messing up user rank calculation and not\nletting you transfer\n\n- Cat shrine not appearing when editing it\n\n- Selecting cats from name not letting you select cats\n\n- Transfer error messages not appearing in some cases\n\n## [2.4.0] - 2022-10-05\n\n### Added\n\n- An option in save management to save the save data without opening the file\nselection dialog\n\n- Option to edit where the config file is located\n\n- A way to enter an officer id or generate a random one when getting the gold\npass. Entering -1 for the officer id will remove the gold pass\n\n### Changed\n\n- Platinum shards max amounts now takes into account your current platinum ticket\namount to make sure you can't go over 9 tickets\n\n- Made catshrine appear when using the edit catshrine level feature and the level\nup dialogs are now skipped\n\n- When pulling using adb the editor will automatically detect currently installed\ngame versions and let you select one to pull. If only 1 game version is installed\nit will just default to that one.\n\n### Fixed\n\n- Selecting cats based on name crashing if entering a cat id too large\n\n- Upgrade cats / special skills crashing the editor if setting the base level to\n0 or a level to be larger than 65535\n\n- Being unable to download a save / pull saves if your default country code is\nlonger than 2 characters. The editor will just ask you to manually enter it\n\n- Treasure groups chapter selection ids being off by 1\n\n## [2.3.0] - 2022-09-14\n\n### Added\n\n- Feature to add enigma stages\n\n- Feature to edit Gamatoto shrine xp / level\n\n- Replaced some unknown values in the save stats + updated parsing for 11.3.0\nand up\n\n### Changed\n\n- Get gold pass will now give the paid version instead of the free trial and\neach subsequent use of the feature will increase the total renewal times by 1\nand wipe the daily catfood stamp count\n\n### Fixed\n\n- File not found error if item_tracker.json is not present\n\n## [2.2.2] - 2022-09-04\n\n### Added\n\n- A new config option to select options with the arrow keys or j and k to select\nsome options. `EDITOR` -> `USE_ARROW_KEYS_FOR_FEATURE_SELECT`\n\n### Fixed\n\n- Default save path being empty, causing the editor to not be able to pull saves\nunless changed\n\n## [2.2.1] - 2022-09-04\n\n### Fixed\n\n- Editor sometimes crashing when saving a file when the file dialog\n\n## [2.2.0] - 2022-09-03\n\n### Added\n\n- Option when selecting cats to only get obtainable cats (Only the cats that\nshow up in the cat guide)\n\n- Option to select cats by name when selecting cats\n\n### Changed\n\n- Config file will now be located in the app data folder / home folder\n\n- Character drop, evolve cats and talents will now be able to use the normal cat\nselecting menu\n\n- You can now select all chapters at once when editing treasure groups\n\n### Fixed\n\n- Wrong chapter being shown when selecting levels\n\n- Editor crashing when entering the name of a category when selecting a feature\n\n## [2.1.1] - 2022-08-17\n\n### Changed\n\n- Split up some features into subcategories e.g Treasures / Levels -> Treasures\n-> Treasure groups. Or Items -> Tickets -> Normal Tickets\n\n### Fixed\n\n- Gamatoto helpers\n\n## [2.1.0] - 2022-08-16\n\n### Added\n\n- The ability to unlock the equip menu\n\n- The ability to upload catfood and other bannable item changes to the ponos\nservers - this is done automatically whenever your save data is saved /\nuploaded. This should in theory prevent bans from catfood and other items,\nbut it seems a bit unreliable so I've kept the warning in the editor\n\n- A feature to claim all user rank rewards (Doesn't give any items)\n\n- A way to select specific gacha banner cats - you need to go to the wiki for\nthe banner you want, and look at the name of the image e.g royal fest = 602\n\n- The ability to get the gold pass\n\n- A feature to create a new account - new iq and token\n\n- A way to clear specific aku stages\n\n- Some configuration options , e.g options to remove max limits, automatically\nsave changes after each edit, etc, the path to the config file is shown at the\ntop of the editor\n\n### Changed\n\n- You can now exit, catfood, rare, plat, and legend tickets after the warning is\nshown\n\n- The editor will now display \"Press enter to exit\" when exiting\n\n- Whenever your inquiry code changes, the editor will upload your catfood and\nother bannable item amounts to the servers - this should prevent bans\n\n- When entering a transfer code, the editor will check for a hex number and when\nentering a confirmation code it will check for a dec number. This should prevent\npeople confusing 0 for O\n\n- Game data will now be downloaded from [here](https://github.com/fieryhenry/BCData)\nwhen needed so that if I want to update the data in the editor, I don't have to\ndo a new release\n\n### Fixed\n\n- Select cats based on rarity being off by 1\n- Evolve cats setting some cats to the first form\n\n## [2.0.2] - 2022-07-08\n\n### Fixed\n\n- Jp not being able to upload save data\n\n## [2.0.1] - 2022-07-04\n\n### Fixed\n\n- Upgrade cats and unlock event stages not working properly when editing all at once\n\n## [2.0.0] - 2022-07-04\n\n### Added\n\n- The ability to upload your save data to the ponos servers and get transfer\nand confirmation codes. (The editor's root requirement is now gone). Although,\nyou'll still need root access if you get banned / elsewhere popup. I haven't\ntested the feature too much so it could lead to bans\n\n- An option to go back in the feature menu\n\n- An automatic updater, if there is a new update, it will ask if you want to\nupdate and if you say yes then it'll try to update automatically\n\n- A way to select `all` talent orbs to edit all at once\n\n- A new tutorial video that shows you how to use the transfer system stuff and\nunban an account [here](https://www.youtube.com/watch?v=Kr6VaLTXOSY)\n\n### Changed\n\n- The fix elsewhere / unban feature, it no longer needs another account.\nYou can still use the old one, now named `Old Fix elsewhere error / Unban\naccount (needs 2 save files)` if you want\n\n- A bunch of the source code. You should now be able to import BCSFE_Python in\nanother python file and access the parser, serialiser, etc. Due to the rewrite,\nsome stuff may be broken. This, and testing, is where the majority of the time\nwent to\n\n- The order of few options, to make the server stuff closer to the top as that's\nwhat most people will be selecting now that no root is needed\n\n### Fixed\n\n- Some adb issues\n\n- More save parsing issues\n\n- Edit dojo score crashing if you haven't been to the dojo yet\n\n- Adding adb to path issue\n\n- Ototo cat cannon not setting the correct value when editing all at once\n\n- Individual treasures feature giving you 49/48 treasures\n\n## [1.8.0.1] - 2022-05-24\n\n### Removed\n\n- Import from a random module that got imported automatically by vscode\n\n## [1.8.0] - 2022-05-24\n\n### Added\n\n- New behemoth stones to get catfruit feature\n- The ability to fix gamatoto from crashing the game\n\n### Fixed\n\n- Some adb issues thanks to [!j0](https://github.com/j0912345)\n- More save parsing issues\n\n## [1.7.1] - 2022-05-20\n\n### Fixed\n\n- Save parsing issue with en 11.5\n\n## [1.7.0] - 2022-05-20\n\n### Added\n\n- The ability to clear catnip challenges / missions\n- The ability to complete cat cannons to certain stages (e.g foundation, style, cannon)\n- The ability to set the Catclaw dojo score (only `Hall of Initiates` atm -\ndon't know if ranked stuff can be save edited)\n- The ability to remove the `Clear \"{stage_name}\" for a chance to get the\nSpecial unit {cat_name}` stage clear rewards when entering Legend Stages\n- The ability to set the `maxed upgrades --> rare tickets` conversion thing to\nallow for unbannable rare tickets to be generated. Run the `trade progress`\nfeature, enter the number of rare tickets you want, go into game and press\nthe `Use All` button in cat storage and then press `Trade for Ticket` .\nThere appears to be nothing in your storage because there is an unobtainable blue\nupgrade / special skill between `power` and `range` and the editor adds that to\nyour storage to allow you to use the `trade` thing, although any other blue\nupgrade also works, as long as it is max level.\n\n### Fixed\n\n- More save parsing issues\n\n## [1.6.2] - 2022-05-03\n\n### Fixed\n\n- Upgrade cats and upgrade blue upgrades crashing the editor\n\n## [1.6.1] - 2022-05-03\n\n### Fixed\n\n- Gauntlets from crashing the editor\n\n## [1.6.0] - 2022-05-03\n\n### Added\n\n- The ability to edit specific treasures for each stage\n\n- The ability to edit groups of treasures (e.g energy drink, aqua crystal)\n\n### Fixed\n\n- More save parsing issues\n- Event stages crashing when selecting `all` for the stage ids\n\n## [1.5.0] - 2022-04-28\n\n### Added\n\n- When exporting to json, the current editor version will be included and so if\njson data from a different editor version is being imported a warning\nmessage will show.\n\n- Option to edit specific stages in a main story chapter\n\n- Option to remove enemy guide entries\n\n### Fixed\n\n- More save parsing issues\n\n- Meow medals not writing properly\n\n- The enemy ids in `unlock/remove enemy guide entries` not being the same as the\nones on the wiki\n\n## [1.4.8] - 2022-04-23\n\n### Fixed\n\n- More save parsing issues\n- Event stages, uncanny, gauntlets not unlocking the next subchapter\n- Outbreaks crashing the editor after being edited\n\n## [1.4.7] - 2022-04-22\n\n### Changed\n\n- It seems like the adb included in the editor doesn't work, and so I've removed\nit, you now need to have adb in your Path environment variable. Tutorial\nin the help videos's description\n\n## [1.4.6] - 2022-04-22\n\n### Changed\n\n- When the editor detects a new version, it will display where to see the changelog\n\n### Fixed\n\n- A small issue relating to meow medals\n\n- Some more parsing errors\n\n## [1.4.5] - 2022-04-22\n\n### Changed\n\n- `adb.exe` is now included in the project, so you should be able to auto-pull\nand push saves without adding it to your `PATH`\n\n### Fixed\n\n- Catfruit crashing\n\n## [1.4.4] - 2022-04-21\n\n### Fixed\n\n- Some saves getting an error when parsing\n\n## [1.4.2 & 1.4.3] - 2022-04-21\n\n### Fixed\n\n- It should correctly auto-install required packages\n\n## [1.4.1] - 2022-04-21\n\n### Fixed\n\n- It should auto-install required packages\n\n## [1.4.0] - 2022-04-21\n\n### Added\n\n- Ability to unlock enemy guide\n\n- Ability to clear cat guide rewards\n\n### Changed\n\n- Made clear tutorial also beat Korea\n\n## [1.3.0] - 2022-04-21\n\n### Added\n\n- Ability to add, upgrade cats, and true form cats in a certain rarity category.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n\n"
  },
  {
    "path": "LOCALIZATION.md",
    "content": "# Localization\n\nSmall tutorial on how to localize the editor into a different language.\n\n## Disclaimer\n\nPlease do not use machine or AI translated text, they will likely make mistakes, especially since\nthere is specific terminology unique to The Battle Cats and the save editor, and if you do not\nknow the language you will not be able to correct them. There are also many other ethical and legal\nissues when using AI that I would like to avoid.\n\nThank you for understanding\n\n## How To\n\n0. If you want to submit a pull request later you should fork the editor (make sure to fork the codeberg repo: <https://codeberg.org/fieryhenry/BCSFE-Python>)\n\n1. Install the editor from source by following [these instructions](https://codeberg.org/fieryhenry/BCSFE-Python#install-from-source)\n  (make sure to change the git clone url to be your fork if you have one)\n\n2. Inside the `src/bcsfe/files/locales/` folder you will find the pre-existing locales, copy the\n  one named `en` and rename it to the code of the language you are translating to\n\n3. Create a file called `metadata.json` inside the folder and edit it to contain the following info:\n\n```json\n{\n  \"authors\": [\"author-1\", \"author2\", \"cool-person3\"],\n  \"name\": \"Name of language (english name of language)\"\n}\n```\n\nFor example the one for Vietnamese looks like this:\n\n```json\n{\n  \"authors\": [\"HungJoesifer\"],\n  \"name\": \"Tiếng Việt (Vietnamese)\"\n}\n````\n\n4. Edit each of the .properties file, translating each value, try to keep the colors the same as\nthe original text. Anything in `{..}` should stay exactly how it is. Anything in `{{..}}` references\nanother key and so can be changed if you want. For more details see [here](https://codeberg.org/fieryhenry/ExampleEditorLocale/).\n\n5. Once you think you have finished, open the editor and edit the config value `Language` and\nselect your language from the list\n\n6. Restart the editor and check that it works, you should also see the details you specified\nin the `metadata.json` file in the opening text\n\n7. Enable the config option to display missing locale keys then restart the editor\n\n8. If everything is correct you shouldn't see any missing keys (extra keys are fine).\n\n9. Once done, push your changes to your fork if you have one and feel free to submit a pull request\nto the codeberg repo. Alternatively you can just zip your locale folder and send it to me or in\nthe #localization channel on discord. (or [matrix](https://matrix.to/#/@fieryhenry:matrix.battlecatsmodding.org))\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "recursive-include src/bcsfe/files *\nrecursive-exclude src/bcsfe/files/game_data *\nrecursive-exclude src/bcsfe/files/map_names *"
  },
  {
    "path": "README.md",
    "content": "# Battle Cats Save File Editor\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/fieryhenry)\n\n[![LiberaPay](https://liberapay.com/assets/widgets/donate.svg)](https://liberapay.com/fieryhenry)\n\nBCSFE is a python command line save editor for The Battle Cats.\n\nJoin the [discord server](https://discord.gg/DvmMgvn5ZB) if you want to suggest\nnew features, report bugs or get help on how to use the editor (please read the\nbelow tutorials first before asking for help).\n\n## Thanks to\n\nLethal's editor for giving me inspiration to start the project and it helped me\nwork out how to patch the save data and edit cf/xp: <https://www.reddit.com/r/BattleCatsCheats/comments/djehhn/editoren/>\n\nBeeven and csehydrogen's free and open source code, which helped me figure out how to\npatch save data: [beeven/battlecats](https://github.com/beeven/battlecats), [csehydrogen/BattleCatsHacker](https://github.com/csehydrogen/BattleCatsHacker)\n\nAnyone who has supported my on [Ko-Fi](https://ko-fi.com/fieryhenry) or [LiberaPay](https://liberapay.com/fieryhenry)\n\nEveryone who's given me saves, which helped to test save loading/saving and to\ntest/develop new features\n\n### Localization\n\n- HungJoesifer for Vietnamese localization\n\n- LinYuAn for Traditional Chinese localization\n\n### Themes\n\n- HungJoesifer for the `discord` inspired theme\n\n## Installation\n\nNote the following tutorials are for the device you wish to run the editor on, not the device\nthat you have the game installed on.\n\nFor example just because you have the game on an android device, does not mean you have to run\nthe editor on it. It is easier to run the editor on a PC / laptop rather than on a mobile device.\n\n### Windows / MacOS\n\n1. Install Python 3.9 or later if you don't already have it: <https://www.python.org/downloads/>\n\n2. Open a terminal such as PowerShell or Command Prompt\n\n3. Run the following command:\n\n```powershell\npy -m pip install bcsfe\n```\n\n4. If you get an error saying that `py` is not a recongnised command, then try:\n\n```powershell\npython -m pip install bcsfe\n```\n\nor\n\n```powershell\npython3 -m pip install bcsfe\n```\n\n5. If you get an error saying `No module named pip`, then run:\n\n```powershell\npy -m ensurepip --upgrade\n```\n\nAgain change `py` for `python` or `python3` if needed. I won't mention this again, so just remember\nthe one which works at keep using that.\n\n5. To run the editor, as long as Python is in your PATH, you should be able to run:\n\n```powershell\nbcsfe\n```\n\n6. If Python is not in your path you'll need to run:\n\n```powershell\npy -m bcsfe\n```\n\nIf 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).\n\n7. To update the editor run:\n\n```powershell\npy -m pip install -U bcsfe\n```\n\n8. To uninstall the editor run:\n\n```powershell\npy -m pip uninstall bcsfe\n```\n\n### Linux\n\n1. Install Python 3.9 or later using your system's package manager if you don't already have it\n\n2. You might have to install pip seperately with a package called `python-pip` or something similar\nor you can run the following command:\n\n```sh\npython3 -m ensurepip --upgrade\n```\n\n3. Depending on your distro you might not be able to install the editor directly using the system\npip and you might need to use pipx (python-pipx) or create a virtual environment manually.\n\n4. Using pipx:\n\n```sh\npipx install bcsfe\n```\n\n5. If `~/.local/bin/` is in your path you should be able to run the editor with the command:\n\n```sh\nbcsfe\n```\n\n6. You may also need to install `tk` with your system package manager to open the\nfile selection dialog. This package may be called `tk` or `python-tk` or `python3-tk`.\n\n7. To update the editor if you are using pipx run:\n\n```sh\npipx upgrade bcsfe\n```\n\n8. To uninstall the editor if you are using pipx run:\n\n```sh\npipx uninstall bcsfe\n```\n\nIf anyone wants to put the editor on the AUR or another package repo, feel free, I'll be happy to\nhelp if needed.\n\n### Android\n\nYou need to install a terminal emulator to be able to install and run Python packages.\n\n[Termux](https://termux.dev/en/) is a good option and is what this tutorial will use.\n\n1. Download Termux, you can either get it from [F-Droid](https://f-droid.org/), or the APK directly\nfrom [GitHub](https://github.com/termux/termux-app?tab=readme-ov-file#github). DO NOT use the\nGoogle Play Store version, as it does not fully work.\n\nI recommend using F-Droid since it can update Termux for you (and it's just a better alternative\nthan using the Google Play Store).\n\nOn F-Droid Termux is called `Termux Terminal emulator with packages`\n\n2. Once Termux is installed, open it and run the following commands:\n\n```sh\ntermux-setup-storage\ntermux-change-repo\npkg update\npkg upgrade\npkg install python python-pip\n```\n\nWhen it asks for a mirror, it doesn't really matter which one you pick, the default single mirror\nworks fine.\n\n3. Install the editor with the following command:\n\n```sh\npip install bcsfe\n```\n\nOr if that doesn't work try:\n\n```sh\npython -m pip install bcsfe\n```\n\n4. Run the editor with the following command:\n\n```sh\nbcsfe\n```\n\nOr if that doesn't work try:\n\n```sh\npython -m bcsfe\n```\n\nNote that the editor might give you warnings about tkinter not being installed, you can just\nignore those as tkinter will not work on mobile. This just means that instead of a graphical file\nselection dialog, you just have to type the file path manually.\n\nFor example to save your save file to your downloads directory, the path might look something like\n`/storage/emulated/0/Download/SAVE_DATA` or `/sdcard/Download/SAVE_DATA`\n\n5. To update the editor run:\n\n```sh\npip install -U bcsfe\n```\n\nOr\n\n```sh\npython -m pip install -U bcsfe\n```\n\n\n5. To uninstall the editor run:\n\n```sh\npip uninstall bcsfe\n```\n\nOr\n\n```sh\npython -m pip uninstall bcsfe\n```\n\n### iOS\n\nI do not have an iOS device, so there is no tutorial. The video that was recommended is now outdated.\nBut for a general overview of what you need to do:\n\n1. Download a-Shell from the App Store\n2. Install the editor with:\n\n```sh\npip install bcsfe\n```\n\n3. Run the editor with:\n\n```sh\nbcsfe\n```\n\nOr if that doesn't work try:\n\n```sh\npython -m bcsfe\n```\n\nOr \n\n```sh\npython3 -m bcsfe\n```\n\n4. To update the editor run:\n\n```sh\npip install -U bcsfe\n```\n\n5. To uninstall the editor run:\n\n```sh\npip uninstall bcsfe\n```\n## Terms of Use\n\nI don't like that I have to have Terms of use but these terms are designed to prevent scams and the\nexploitation of users.\n\nBy using the editor you agree to the following:\n\nIf you are using the editor to run a paid service that profits off of the editor\n(e.g a service to provide people with hacked accounts, or a paid discord bot to edit people's accounts,\netc) you must make it very clear that you are using this save editor.\n\nThis should be done by linking this Codeberg page, and explicitly stating that the tool you are\nusing is available for free and that they don't need to use your service to hack their account.\n\nThis information needs to be visible and something the customer agrees to **before** any payment is made.\n\nThis also includes paid services which claim to teach people \"How To Hack The Battle Cats\". In those\ncases, this still applies, so you still need to state and have the customer acknowledge the things\nI said above.\n\nFree services / derivative works (such as a third party discord bot or editor gui) are fine to use\nthe editor under the hood as long as you abide by the [License](#license). Basically if you are\ndistributing a program which uses the editor, you need to license your own program under the GPL\nor a compatible license (basically make it open source / free software too).\n\nAlso if you **are** profiting from the editor, it would be greatly appreciated if you could\ngive back something and support me.\n\n## Usage\n\nOnce you have installed and ran the editor, you can now begin to edit your save file!\n\n1. In `The Battle Cats` enter the `Change Account / Device` menu in the `Settings` on the main menu.\n\n2. Then enter the `Change Device` -> `Retrieve Data from Old Device` menu.\n\n3. Then click / tap `Save Data to Server`, this should give you a transfer code and a confirmation\ncode.\n\n4. In the editor use the option called `Download save file using transfer and\nconfirmation code` by entering the number `1`\n\n5. Enter your transfer code\n\n6. Enter your confirmation code\n\n7. Select the country code that you are using, `en`=english,\n`kr`=korean, `jp`=japanese, `tw`=taiwanese.\n\nNote that `en` also includes the `it`, `es`, `fr`, `th`, and `de` translations.\n\n8. Edit what you want. Note that in most cases, if you want to exit the current\n   input you can enter `q` and press enter to go back to the previous menu\n\n9. In the editor, go into the `Save Management` category and select `Save changes and upload to\ngame servers (get transfer and confirmation codes)`. It may take some time, it\nmay also fail, if it does then try again.\n\n10. This should give you a new transfer code and a new confirmation code.\n\n11. Back in-game, tap the `Close Game` button, then tap `Cancel Data Transfer` (and also possibly\n`Start Game From Beginning`)\n\n12. Go back into the `Change Account / Device` menu and then go into the `Resume Data Transfer` ->\n`Transfer Data to New Device` menu\n\n13. Enter the new codes, and tap `Resume Transfer`\n\n14. Then done! You should see your edits in-game.\n\n15. Every time that you want to make an edit to your save, you will have to re-upload it to the\ngame servers in the editor and re-download it in-game, the saves aren't automatically linked together.\n\nApparently doing the Google Account / Apple Account link limits the number of data transfers you can\ndo within a certain time. So to be safe, I would avoid linking your account.\n\n### Using a rooted device via adb\n\n1. Add adb to your PATH environment variable, or edit the config to set ADB path editor config option\n  to the full path of the adb executable. You can download adb from\n  [platform-tools](https://developer.android.com/studio/releases/platform-tools)\n\n1. Open the editor and select the option named `Pull save file from device\nusing adb` and enter your game version, or select the option named\n`Select save file from file` and select a copy of your save data\n\n1. Edit what you want\n\n1. Go into save management and select an option to push save data to the game\n\n1. Enter the game and you should see changes\n\n### Using a rooted device directly\n\n1. You need to be running the editor on the device itself, so you'll need to\nfollow the [Android tutorial](#android) to install the editor\n\n1. You may have to run the editor with `sudo python -m bcsfe` or something, so you might have to\nsetup the termux root repo and run `pkg install sudo`\n\n1. In the editor select the option named `Pull save file from root storage`\n\n1. Edit what you want\n\n1. Go into save management and select an option to push save data to the game\n\n1. Enter the game and you should see changes\n\n\n### How to unban your account\n\n1. Select the option in `Account` to `Unban account` or\njust upload the save data to the game servers again\n\n1. It may take some time but after, you should be able to choose one of the\noptions in save management to push the save data to the game.\n\n#### How to prevent a ban in the future\n\n- Instead of editing in platinum tickets use the `Platinum Shards` feature\n\n- Instead of editing in rare tickets use the `Normal Ticket Max Trade Progress\n(allows for unbannable rare tickets)` feature\n\n- Instead of hacking in cat food, just edit everything in that you can buy with\ncat food, e.g battle items, catamins, xp, energy refills (leaderships), etc.\nIf you really want catfood then you can clear and unclear catnip missions with\nthe feature `Catnip Challenges / Missions` then entering 1 when asked.\nYou'll need to collect the catfood in-game after each clear though\n\n- Instead of hacking in tickets, just hack in the cats/upgrades you want directly\n\n### Install from source\n\nIf you want the latest features then you can install the editor from the git repo.\n\n1. Download git:\n    - Windows: [Git](https://git-scm.com/downloads)\n    - Linux: (use package manager, e.g `sudo apt-get install git` or `sudo pacman -S git`)\n    - Android: Termux: `pkg install git`\n    - iOS: a-Shell should already include it\n\n2. Run the following commands: (You may have to replace `py` with `python` or `python3`)\n\n```sh\ngit clone https://codeberg.org/fieryhenry/BCSFE-Python.git\ncd BCSFE-Python\npy -m pip install -e .\npy -m bcsfe\n```\n\nThen if you want the latest changes you only need to run `git pull` in the downloaded\n`BCSFE-Python` folder. (use `cd` to change the folder)\n\nAlternatively you can use pip directly, although it won't auto-update with the latest\ngit commits.\n\n```sh\npy -m pip install -U git+https://codeberg.org/fieryhenry/BCSFE-Python.git\npy -m bcsfe\n```\n\nAgain, you might need change `py` for `python` or `python3`\n\nIf you want to use the editor again all you need to do is run the `py -m bcsfe` command\n\n## Documentation\n\n- [Custom Editor Locales](https://codeberg.org/fieryhenry/ExampleEditorLocale)\n- [Custom Editor Themes](https://codeberg.org/fieryhenry/ExampleEditorTheme)\n\nI only have documentation for the locales and themes atm, but I will probably\nadd more documentation in the future.\n\n## Contributing\n\nIf you want to contribute to the BCSFE, I recommend joining the [Discord Server](https://discord.gg/DvmMgvn5ZB) and starting a\ndiscussion in #dev-chat, or create an issue in this repo, or a draft pull request.\n\nIf you need help with reverse engineering the save file, I have a basic starting guide here:\n<https://codeberg.org/fieryhenry/bc_ree>.\n\nIf you want to localize the editor see [here](./LOCALIZATION.md).\n\n## License\n\nBCSFE is licensed under the GNU GPLv3 which can be read [here](https://www.gnu.org/licenses/gpl-3.0.en.html).\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"bcsfe\"\nauthors = [{ name = \"fieryhenry\" }]\ndescription = \"A save file editor for The Battle Cats\"\nlicense = \"GPL-3.0-or-later\"\nreadme = \"README.md\"\nrequires-python = \">=3.9\"\nclassifiers = [\n  \"Development Status :: 4 - Beta\",\n  \"Intended Audience :: End Users/Desktop\",\n  \"Topic :: Utilities\",\n  \"Programming Language :: Python :: 3\",\n  \"Programming Language :: Python :: 3.9\",\n  \"Operating System :: OS Independent\",\n]\ndependencies = [\n  \"aenum\",\n  \"colored==1.4.4\",\n  \"pyjwt\",\n  \"requests\",\n  \"pyyaml\",\n  \"beautifulsoup4\",\n  \"argparse\",\n]\ndynamic = [\"version\"]\nkeywords = [\"battle cats\", \"save editor\", \"hacking\"]\n\n[project.urls]\nHomepage = \"https://codeberg.org/fieryhenry/BCSFE-Python\"\nRepository = \"https://codeberg.org/fieryhenry/BCSFE-Python\"\nIssues = \"https://codeberg.org/fieryhenry/BCSFE-Python/issues\"\nChangelog = \"https://codeberg.org/fieryhenry/BCSFE-Python/raw/branch/main/CHANGELOG.md\"\n\n[tool.setuptools.dynamic]\nversion = { attr = \"bcsfe.__version__\" }\n\n[tool.setuptools]\npackage-dir = { \"\" = \"src\" }\n\n[project.scripts]\nbcsfe = \"bcsfe:run\"\n"
  },
  {
    "path": "requirements.txt",
    "content": "aenum==3.1.16\ncolored==1.4.4\nPyJWT==2.12.1\nPyYAML==6.0.2\nRequests==2.33.1\nbeautifulsoup4==4.13.4\nargparse==1.4.0\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup\n\nsetup()\n"
  },
  {
    "path": "src/bcsfe/__init__.py",
    "content": "__version__ = \"3.3.0\"\n\nfrom bcsfe import core, cli\n\n\n__all__ = [\"core\", \"cli\"]\n\n\ndef run():\n    from bcsfe import __main__\n\n    __main__.main()\n"
  },
  {
    "path": "src/bcsfe/__main__.py",
    "content": "from __future__ import annotations\nimport traceback\n\nfrom bcsfe import cli\n\nfrom bcsfe import core\n\nimport bcsfe\nimport argparse\n\n\ndef main():\n    parser = argparse.ArgumentParser(\"bcsfe\")\n\n    parser.add_argument(\n        \"--version\", \"-v\", action=\"store_true\", help=\"display the version and exit\"\n    )\n    parser.add_argument(\n        \"--input-path\", \"-i\", type=str, help=\"input path to save file to edit\"\n    )\n    parser.add_argument(\n        \"--game-data-dir\", \"-g\", type=str, help=\"path to store the game data to\"\n    )\n    parser.add_argument(\n        \"--transfer-backup-path\",\n        type=str,\n        help=\"path to save the backup SAVE_DATA after transfering to\",\n    )\n    parser.add_argument(\n        \"--config-path\",\n        \"-c\",\n        type=str,\n        default=None,\n        help=f\"path to the config file. If unspecified defaults to {core.Config.get_config_path()}\",\n    )\n    parser.add_argument(\n        \"--log-path\",\n        \"-l\",\n        type=str,\n        default=None,\n        help=f\"path to the log file. If unspecified defaults to {core.Logger.get_log_path()}\",\n    )\n\n    args = parser.parse_args()\n    if args.version:\n        print(bcsfe.__version__)\n        exit()\n\n    if args.config_path is not None:\n        core.set_config_path(core.Path(args.config_path))\n\n    if args.log_path is not None:\n        core.set_log_path(core.Path(args.log_path))\n\n    if args.transfer_backup_path is not None:\n        core.set_transfer_backup_path(core.Path(args.transfer_backup_path))\n\n    if args.game_data_dir is not None:\n        core.set_game_data_path(core.Path(args.game_data_dir))\n\n    core.core_data.init_data()\n\n    try:\n        cli.main.Main().main(args.input_path)\n    except KeyboardInterrupt:\n        cli.main.Main.leave()\n    except Exception as e:\n        tb = traceback.format_exc()\n        cli.color.ColoredText.localize(\n            \"error\", error=e, version=bcsfe.__version__, traceback=tb\n        )\n        try:\n            cli.main.Main.exit_editor()\n        except Exception:\n            pass\n        except KeyboardInterrupt:\n            pass\n\n\nmain()\n"
  },
  {
    "path": "src/bcsfe/cli/__init__.py",
    "content": "from bcsfe.cli import (\n    color,\n    dialog_creator,\n    main,\n    file_dialog,\n    feature_handler,\n    save_management,\n    server_cli,\n    edits,\n    recent_saves,\n)\n\n__all__ = [\n    \"color\",\n    \"dialog_creator\",\n    \"main\",\n    \"file_dialog\",\n    \"feature_handler\",\n    \"save_management\",\n    \"server_cli\",\n    \"edits\",\n    \"recent_saves\",\n]\n"
  },
  {
    "path": "src/bcsfe/cli/color.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom aenum import NamedConstant  # type: ignore\nimport colored  # type: ignore\nfrom bcsfe import core\n\n\nclass ColorHex(NamedConstant):\n    GREEN = \"#008000\"\n    G = GREEN\n    RED = \"#FF0000\"\n    R = RED\n    DARK_YELLOW = \"#D7C32A\"\n    DY = DARK_YELLOW\n    BLACK = \"#000000\"\n    BL = BLACK\n    WHITE = \"#FFFFFF\"\n    W = WHITE\n    CYAN = \"#00FFFF\"\n    C = CYAN\n    DARK_GREY = \"#A9A9A9\"\n    DG = DARK_GREY\n    BLUE = \"#0000FF\"\n    B = BLUE\n    YELLOW = \"#FFFF00\"\n    Y = YELLOW\n    MAGENTA = \"#FF00FF\"\n    M = MAGENTA\n    DARK_BLUE = \"#00008B\"\n    DB = DARK_BLUE\n    DARK_CYAN = \"#008B8B\"\n    DC = DARK_CYAN\n    DARK_MAGENTA = \"#8B008B\"\n    DM = DARK_MAGENTA\n    DARK_RED = \"#8B0000\"\n    DR = DARK_RED\n    DARK_GREEN = \"#006400\"\n    DGN = DARK_GREEN\n    LIGHT_GREY = \"#D3D3D3\"\n    LG = LIGHT_GREY\n    ORANGE = \"#FFA500\"\n    O = ORANGE\n\n    @staticmethod\n    def from_name(name: str) -> str:\n        if name == \"\":\n            return \"\"\n        return getattr(ColorHex, name.upper())\n\n\nclass ColorHelper:\n    def __init__(self):\n        self.theme_handler = core.core_data.theme_manager\n\n    def get_color(self, color_name: str) -> str:\n        try:\n            first_char = color_name[0]\n        except IndexError:\n            return \"\"\n        if first_char == \"#\":\n            return color_name\n        if first_char == \"@\":\n            try:\n                second_char = color_name[1]\n            except IndexError:\n                return \"\"\n            try:\n                third_char = color_name[2]\n            except IndexError:\n                third_char = \"\"\n            if second_char == \"p\":\n                return self.theme_handler.get_primary_color()\n            if second_char == \"s\" and third_char != \"u\":\n                return self.theme_handler.get_secondary_color()\n            if second_char == \"t\":\n                return self.theme_handler.get_tertiary_color()\n            if second_char == \"q\":\n                return self.theme_handler.get_quaternary_color()\n            if second_char == \"e\":\n                return self.theme_handler.get_error_color()\n            if second_char == \"w\":\n                return self.theme_handler.get_warning_color()\n            if second_char == \"s\" and third_char == \"u\":\n                return self.theme_handler.get_success_color()\n            return self.theme_handler.get_theme_color(color_name[1:])\n        try:\n            return ColorHex.from_name(color_name)\n        except AttributeError:\n            return \"\"\n\n\nclass ColoredText:\n    def __init__(self, string: str, end: str = \"\\n\") -> None:\n        string = string.replace(\"\\\\n\", \"\\n\")\n        self.string = string\n        self.end = end\n        self.color_helper = ColorHelper()\n        self.display(string)\n\n    def display(self, string: str) -> None:\n        text_data = self.parse(string)\n        for i, (text, color) in enumerate(text_data):\n            if i == len(text_data) - 1:\n                text += self.end\n            if color == \"\":\n                print(text, end=\"\")\n            else:\n                try:\n                    fg = colored.fg(color)  # type: ignore\n                except Exception:\n                    print(text, end=\"\")\n                    continue\n                print(colored.stylize(text, fg), end=\"\")  # type: ignore\n\n    @staticmethod\n    def localize(string: str, escape: bool = True, **kwargs: Any) -> ColoredText:\n        return ColoredText(\n            core.core_data.local_manager.get_key(string, escape=escape, **kwargs)\n        )\n\n    def parse(self, txt: str) -> list[tuple[str, str]]:\n        txt = \"<@p>\" + txt + \"</>\"\n        output: list[tuple[str, str]] = []\n        i = 0\n        tags: list[str] = []\n        inside_tag = False\n        in_closing_tag = False\n        tag_text = \"\"\n        text = \"\"\n        special_chars = core.LocalManager.get_special_chars()\n        while i < len(txt):\n            char = txt[i]\n            if char == \"\\\\\" and i + 1 < len(txt) and txt[i + 1] in special_chars:\n                i += 1\n                char = txt[i]\n                text += char\n                i += 1\n                continue\n            if tags:\n                tag = tags[-1]\n            else:\n                tag = \"\"\n            if char == \">\" and inside_tag:\n                inside_tag = False\n                if not in_closing_tag:\n                    tags.append(tag_text)\n                if in_closing_tag:\n                    in_closing_tag = False\n                tag_text = \"\"\n            if char == \"<\" and not inside_tag:\n                inside_tag = True\n                if text:\n                    color = self.color_helper.get_color(tag)\n                    output.append((text, color))\n                    text = \"\"\n                    tag_text = \"\"\n            if char == \"/\" and inside_tag:\n                in_closing_tag = True\n                if tags:\n                    tags.pop()\n            if not inside_tag and char != \">\" and char != \"<\":\n                text += char\n            if inside_tag and char != \"<\" and char != \">\":\n                tag_text += char\n            i += 1\n        return output\n\n\nclass ColoredInput:\n    def __init__(self, end: str = \"\") -> None:\n        self.end = end\n\n    def get(self, display_string: str) -> str:\n        ColoredText(display_string, end=self.end)\n        return input()\n\n    def localize(self, string: str, escape: bool = True, **kwargs: Any) -> str:\n        text = core.core_data.local_manager.get_key(string, escape=escape, **kwargs)\n        return self.get(text)\n"
  },
  {
    "path": "src/bcsfe/cli/dialog_creator.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\nclass RangeInput:\n    def __init__(self, max: int | None = None, min: int = 0):\n        self.max = max\n        self.min = min\n\n    def clamp_value(self, value: int) -> int:\n        if self.max is None:\n            return max(value, self.min)\n        return max(min(value, self.max), self.min)\n\n    def get_input_locale(\n        self,\n        dialog: str,\n        perameters: dict[str, int | str],\n        escape: bool = True,\n    ) -> list[int] | None:\n        user_input = color.ColoredInput(end=\"\").localize(\n            dialog, escape=escape, **perameters\n        )\n        return self.parse(user_input)\n\n    def parse(self, user_input: str) -> list[int] | None:\n        if user_input == \"\":\n            return []\n        if user_input == core.core_data.local_manager.get_key(\"quit_key\"):\n            return None\n        parts = user_input.split(\" \")\n        ids: list[int] = []\n        all_text = core.core_data.local_manager.get_key(\"all\")\n        for part in parts:\n            if \"-\" in part and len(part.split(\"-\")) == 2:\n                lower, upper = part.split(\"-\")\n                try:\n                    lower = int(lower)\n                    upper = int(upper)\n                except ValueError:\n                    continue\n                if lower > upper:\n                    lower, upper = upper, lower\n                if self.max is not None:\n                    lower = max(lower, self.min)\n                    upper = min(upper, self.max)\n                else:\n                    lower = max(lower, self.min)\n                ids.extend(range(lower, upper + 1))\n            elif part.lower() == all_text.lower() and self.max is not None:\n                ids.extend(range(self.min, self.max + 1))\n            else:\n                try:\n                    part = int(part)\n                except ValueError:\n                    continue\n                if self.max is not None:\n                    part = max(part, self.min)\n                    part = min(part, self.max)\n                else:\n                    part = max(part, self.min)\n                ids.append(part)\n        return ids\n\n\nclass IntInput:\n    def __init__(\n        self,\n        max: int | None = None,\n        min: int = 0,\n        default: int | None = None,\n        signed: bool = True,\n        bit_count: int = 32,\n        ensure_max: bool = False,\n    ):\n        self.signed = signed\n        self.bit_count = bit_count\n        self.max = self.get_max_value(max, signed, bit_count, ensure_max)\n        self.min = min\n        self.default = default\n\n    @staticmethod\n    def get_max_value(\n        max: int | None,\n        signed: bool = True,\n        bit_count: int = 32,\n        ensure_max: bool = False,\n    ) -> int:\n        disable_maxes = (\n            core.core_data.config.get_bool(core.ConfigKey.DISABLE_MAXES)\n            and not ensure_max\n        )\n        if signed:\n            bit_count -= 1\n        max_int = (2**bit_count) - 1\n        if disable_maxes or max is None:\n            return max_int\n        return min(max, max_int)\n\n    def clamp_value(self, value: int) -> int:\n        return max(min(value, self.max), self.min)\n\n    def get_input(\n        self,\n        localization_key: str,\n        perameters: dict[str, int | str],\n        escape: bool = True,\n    ) -> tuple[int | None, str]:\n        user_input = color.ColoredInput(end=\"\").localize(\n            localization_key, escape=escape, **perameters\n        )\n        if user_input == \"\" and self.default is not None:\n            return self.default, user_input\n        try:\n            user_input_i = int(user_input)\n        except ValueError:\n            return None, user_input\n\n        return self.clamp_value(user_input_i), user_input\n\n    def get_input_locale_while(\n        self, dialog: str, perameters: dict[str, int | str], escape: bool = True\n    ) -> int | None:\n        while True:\n            int_val, user_input = self.get_input(dialog, perameters, escape=escape)\n            if int_val is not None:\n                return int_val\n            if user_input == core.core_data.local_manager.get_key(\"quit_key\"):\n                return None\n\n    def get_input_locale(\n        self, localization_key: str | None, perameters: dict[str, int | str]\n    ) -> tuple[int | None, str]:\n        if localization_key is None:\n            if self.default is not None:\n                perameters = {\n                    \"min\": self.min,\n                    \"max\": self.max,\n                    \"default\": self.default,\n                }\n                localization_key = \"input_int_default\"\n            else:\n                perameters = {\"min\": self.min, \"max\": self.max}\n                localization_key = \"input_int\"\n        return self.get_input(localization_key, perameters)\n\n    def get_basic_input_locale(self, localization_key: str, perameters: dict[str, Any]):\n        try:\n            user_input = int(\n                color.ColoredInput(end=\"\").localize(localization_key, **perameters)\n            )\n        except ValueError:\n            return None\n        return user_input\n\n\nclass ListOutput:\n    def __init__(\n        self,\n        strings: list[str],\n        ints: list[int] | list[str],\n        dialog: str | None = None,\n        perameters: dict[str, Any] | None = None,\n        start_index: int = 1,\n        localize_elements: bool = True,\n    ):\n        self.strings = strings\n        self.ints = ints\n        self.dialog = dialog\n        if perameters is None:\n            perameters = {}\n        self.perameters = perameters\n        self.start_index = start_index\n        self.localize_elements = localize_elements\n\n    def get_output(self, dialog: str | None, strings: list[str]) -> str:\n        end_string = \"\"\n        if dialog is not None:\n            end_string = core.core_data.local_manager.get_key(dialog, **self.perameters)\n        end_string += \"\\n\"\n        for i, string in enumerate(strings):\n            try:\n                int_string = str(self.ints[i])\n            except IndexError:\n                int_string = \"\"\n\n            string = string.replace(\"{int}\", int_string)\n            end_string += f\" <@s>{i + self.start_index}.</> <@t>{string}</>\\n\"\n        end_string = end_string.strip(\"\\n\")\n        return end_string\n\n    def display(self, dialog: str | None, strings: list[str]) -> None:\n        output = self.get_output(dialog, strings)\n        color.ColoredText(output)\n\n    def display_locale(self, remove_alias: bool = False) -> None:\n        dialog = \"\"\n        if self.dialog is not None:\n            dialog = core.core_data.local_manager.get_key(self.dialog)\n        new_strings: list[str] = []\n        for string in self.strings:\n            if self.localize_elements:\n                string_ = core.core_data.local_manager.get_key(string)\n            else:\n                string_ = string\n            if remove_alias:\n                string_ = core.core_data.local_manager.get_all_aliases(string_)[0]\n            new_strings.append(string_)\n\n        self.display(dialog, new_strings)\n\n    def display_non_locale(self) -> None:\n        self.display(self.dialog, self.strings)\n\n\nclass ChoiceInput:\n    def __init__(\n        self,\n        items: list[str],\n        strings: list[str],\n        ints: list[int] | list[str],\n        perameters: dict[str, int | str],\n        dialog: str,\n        single_choice: bool = False,\n        remove_alias: bool = False,\n        display_all_at_once: bool = True,\n        start_index: int = 1,\n        localize_options: bool = True,\n    ):\n        self.items = items\n        self.strings = strings\n        self.ints = ints\n        self.perameters = perameters\n        self.dialog = dialog\n        self.is_single_choice = single_choice\n        self.remove_alias = remove_alias\n        self.display_all_at_once = display_all_at_once\n        self.start_index = start_index\n        self.localize_options = localize_options\n\n    @staticmethod\n    def from_reduced(\n        items: list[str],\n        ints: list[int] | list[str] | None = None,\n        perameters: dict[str, int | str] | None = None,\n        dialog: str | None = None,\n        single_choice: bool = False,\n        remove_alias: bool = False,\n        display_all_at_once: bool = True,\n        start_index: int = 1,\n        localize_options: bool = True,\n    ) -> ChoiceInput:\n        if perameters is None:\n            perameters = {}\n        if ints is None:\n            ints = []\n        if dialog is None:\n            dialog = \"\"\n        return ChoiceInput(\n            items.copy(),\n            items.copy(),\n            ints.copy(),\n            perameters.copy(),\n            dialog,\n            single_choice,\n            remove_alias,\n            display_all_at_once,\n            start_index,\n            localize_options,\n        )\n\n    def get_input(self) -> tuple[int | None, str]:\n        if len(self.strings) == 0:\n            return None, \"\"\n        if len(self.strings) == 1:\n            return self.get_min_value(), \"\"\n        ListOutput(\n            self.strings,\n            self.ints,\n            start_index=self.start_index,\n            localize_elements=self.localize_options,\n        ).display_locale(self.remove_alias)\n        return IntInput(\n            self.get_max_value(), self.get_min_value(), ensure_max=True\n        ).get_input_locale(self.dialog, self.perameters)\n\n    def get_input_while(self) -> int | None:\n        if len(self.strings) == 0:\n            return None\n        while True:\n            int_val, user_input = self.get_input()\n            if int_val is not None:\n                return int_val\n            if user_input == core.core_data.local_manager.get_key(\"quit_key\"):\n                return None\n            for i, string in enumerate(self.strings):\n                if self.localize_options:\n                    string = core.core_data.local_manager.get_key(string)\n                if string.lower().strip() == user_input.lower().strip():\n                    return i + self.start_index\n\n    def get_max_value(self) -> int:\n        return len(self.strings) + self.start_index - 1\n\n    def get_min_value(self) -> int:\n        return self.start_index\n\n    def get_input_locale(self, localized: bool = True) -> tuple[list[int] | None, bool]:\n        if len(self.strings) == 0:\n            return [], False\n        if len(self.strings) == 1:\n            return [self.get_min_value()], False\n        if not self.is_single_choice and self.display_all_at_once:\n            if localized:\n                self.strings.append(\"all_at_once\")\n            else:\n                self.strings.append(core.core_data.local_manager.get_key(\"all_at_once\"))\n        if localized:\n            ListOutput(\n                self.strings,\n                self.ints,\n                start_index=self.start_index,\n                localize_elements=self.localize_options,\n            ).display_locale()\n        else:\n            ListOutput(\n                self.strings,\n                self.ints,\n                start_index=self.start_index,\n                localize_elements=self.localize_options,\n            ).display_non_locale()\n        key = \"input_many\"\n        if self.is_single_choice:\n            key = \"input_single\"\n        dialog = core.core_data.local_manager.get_key(key).format(\n            min=self.get_min_value(), max=self.get_max_value()\n        )\n        usr_input = color.ColoredInput().get(dialog).strip().split(\" \")\n        int_vals: list[int] = []\n        for inp in usr_input:\n            try:\n                value = int(inp)\n                if value > self.get_max_value() or value < self.get_min_value():\n                    raise ValueError\n                int_vals.append(value)\n            except ValueError:\n                if inp == core.core_data.local_manager.get_key(\"quit_key\"):\n                    return None, False\n\n                cont = False\n                for i, string in enumerate(self.strings):\n                    if self.localize_options:\n                        string = core.core_data.local_manager.get_key(string)\n                    if string.lower().strip() == inp.lower().strip():\n                        int_vals.append(i + self.start_index)\n                        cont = True\n                        break\n\n                if cont:\n                    continue\n\n                color.ColoredText.localize(\n                    \"invalid_input_int\",\n                    min=self.get_min_value(),\n                    max=self.get_max_value(),\n                )\n        if (\n            self.get_max_value() in int_vals\n            and not self.is_single_choice\n            and self.display_all_at_once\n        ):\n            return list(range(self.get_min_value(), self.get_max_value())), True\n\n        if self.is_single_choice and len(int_vals) > 1:\n            int_vals = [int_vals[0]]\n\n        return int_vals, False\n\n    def get_input_locale_while(self) -> list[int] | None:\n        if len(self.strings) == 0:\n            return []\n        if len(self.strings) == 1:\n            return [self.get_min_value()]\n        while True:\n            int_vals, all_at_once = self.get_input_locale()\n            if int_vals is None:\n                return None\n            if all_at_once:\n                return int_vals\n            if len(int_vals) == 0:\n                continue\n            if len(int_vals) == 1 and int_vals[0] == 0:\n                return []\n            return int_vals\n\n    def multiple_choice(\n        self, localized_options: bool = True\n    ) -> tuple[list[int] | None, bool]:\n        color.ColoredText.localize(self.dialog, True, **self.perameters)\n        user_input, all_at_once = self.get_input_locale(localized_options)\n        if user_input is None:\n            return None, all_at_once\n        return [i - self.start_index for i in user_input], all_at_once\n\n    def single_choice(self) -> int | None:\n        return self.get_input_while()\n\n    def get(self) -> tuple[int | None | list[int], bool]:\n        if self.is_single_choice:\n            return self.single_choice(), False\n        return self.multiple_choice()\n\n\nclass MultiEditor:\n    def __init__(\n        self,\n        group_name: str,\n        items: list[str],\n        strings: list[str],\n        ints: list[int] | None,\n        max_values: list[int] | int | None,\n        perameters: dict[str, int | str] | None,\n        dialog: str,\n        single_choice: bool = False,\n        signed: bool = True,\n        group_name_localized: bool = False,\n        cumulative_max: bool = False,\n        bit_count: int = 32,\n    ):\n        self.items = items\n        self.strings = strings\n        self.ints = ints\n        self.bit_count = bit_count\n        if self.ints is not None:\n            total_ints = len(self.ints)\n        else:\n            total_ints = len(self.strings)\n        if max_values is None:\n            max_values_ = [None] * total_ints\n        elif isinstance(max_values, int):\n            max_values_ = [max_values] * total_ints\n        else:\n            max_values_ = max_values\n        self.max_values = max_values_\n        if perameters is None:\n            perameters = {}\n        self.perameters = perameters\n        if group_name_localized:\n            self.perameters[\"group_name\"] = core.core_data.local_manager.get_key(\n                group_name\n            )\n        else:\n            self.perameters[\"group_name\"] = group_name\n        self.dialog = dialog\n        self.is_single_choice = single_choice\n        self.signed = signed\n        self.cumulative_max = cumulative_max\n\n    @staticmethod\n    def from_reduced(\n        group_name: str,\n        items: list[str],\n        ints: list[int] | None,\n        max_values: list[int] | int | None,\n        group_name_localized: bool = False,\n        dialog: str = \"input\",\n        cumulative_max: bool = False,\n        items_localized: bool = False,\n    ):\n        if items_localized:\n            for i, item in enumerate(items):\n                items[i] = core.core_data.local_manager.get_key(item)\n        text: list[str] = []\n        for item_name in items:\n            if ints is not None:\n                text.append(f\"{item_name} <@q>: {{int}}</>\")\n            else:\n                text.append(f\"{item_name}\")\n        return MultiEditor(\n            group_name,\n            items,\n            text,\n            ints,\n            max_values,\n            None,\n            dialog,\n            group_name_localized=group_name_localized,\n            cumulative_max=cumulative_max,\n        )\n\n    def edit(self) -> list[int]:\n        choices, all_at_once = ChoiceInput(\n            self.items,\n            self.strings,\n            self.ints or [],  # type: ignore\n            self.perameters,\n            \"select_edit\",\n        ).get()\n        if choices is None:\n            return self.ints or []\n        if isinstance(choices, int):\n            choices = [choices]\n        if all_at_once:\n            return self.edit_all(choices)\n        return self.edit_one(choices)\n\n    def edit_all(self, choices: list[int]) -> list[int]:\n        max_max_value = 0\n        for choice in choices:\n            if choice >= len(self.max_values):\n                max_value = None\n            else:\n                max_value = self.max_values[choice]\n            if max_value is None:\n                max_value = IntInput.get_max_value(\n                    max_value, self.signed, self.bit_count\n                )\n            max_max_value = max(max_max_value, max_value)\n        if self.cumulative_max:\n            max_max_value = max_max_value // len(choices)\n        usr_input = IntInput(max_max_value, default=None).get_input_locale_while(\n            self.dialog + \"_all\",\n            {\n                \"name\": self.perameters[\"group_name\"],\n                \"max\": max_max_value,\n            },\n        )\n        if usr_input is None:\n            return self.ints or []\n        ints = self.ints or [0] * len(self.strings)\n\n        for choice in choices:\n            if choice >= len(self.max_values):\n                max_value = None\n            else:\n                max_value = self.max_values[choice]\n            max_value = IntInput.get_max_value(max_value, self.signed, self.bit_count)\n            value = min(usr_input, max_value)\n            ints[choice] = value\n            if self.ints is not None:\n                color.ColoredText.localize(\n                    \"value_changed\",\n                    name=self.items[choice],\n                    value=value,\n                )\n\n        return ints\n\n    def edit_one(self, choices: list[int]) -> list[int]:\n        ints = self.ints or [0] * len(self.strings)\n\n        for choice in choices:\n            if choice >= len(self.max_values):\n                max_value = None\n            else:\n                max_value = self.max_values[choice]\n            if max_value is None:\n                max_value = IntInput.get_max_value(\n                    max_value, self.signed, self.bit_count\n                )\n\n            if self.cumulative_max:\n                max_value -= sum(ints) - ints[choice]\n                max_value = max(max_value, 0)\n\n            item = self.items[choice]\n            usr_input = IntInput(\n                max_value, default=ints[choice]\n            ).get_input_locale_while(\n                self.dialog,\n                {\"name\": item, \"value\": ints[choice], \"max\": max_value},\n                escape=False,\n            )\n            if usr_input is None:\n                continue\n            ints[choice] = usr_input\n            color.ColoredText.localize(\n                \"value_changed\", name=item, value=ints[choice], escape=False\n            )\n        return ints\n\n\nclass SingleEditor:\n    def __init__(\n        self,\n        item: str,\n        value: int,\n        max_value: int | None = None,\n        min_value: int = 0,\n        signed: bool = True,\n        localized_item: bool = False,\n        remove_alias: bool = False,\n        bit_count: int = 32,\n    ):\n        if localized_item:\n            item = core.core_data.local_manager.get_key(item)\n        if remove_alias:\n            item = core.core_data.local_manager.get_all_aliases(item)[0]\n        self.item = item\n        self.value = value\n        self.max_value = max_value\n        self.min_value = min_value\n        self.signed = signed\n        self.bit_count = bit_count\n\n    def edit(self, escape_text: bool = True) -> int:\n        max_value = IntInput.get_max_value(self.max_value, self.signed, self.bit_count)\n        if self.max_value is None:\n            dialog = \"input_non_max\"\n        elif self.min_value != 0:\n            dialog = \"input_min\"\n        else:\n            dialog = \"input\"\n        usr_input = IntInput(\n            max_value,\n            self.min_value,\n            default=self.value,\n            signed=self.signed,\n            bit_count=self.bit_count,\n        ).get_input_locale_while(\n            dialog,\n            {\n                \"name\": self.item,\n                \"value\": self.value,\n                \"max\": max_value,\n                \"min\": self.min_value,\n            },\n            escape=escape_text,\n        )\n        if usr_input is None:\n            return self.value\n        print()\n        color.ColoredText.localize(\n            \"value_changed\", name=self.item, value=usr_input, escape=escape_text\n        )\n        return usr_input\n\n\nclass StringInput:\n    def __init__(self, default: str = \"\"):\n        self.default = default\n\n    def get_input_locale_while(\n        self, key: str, perameters: dict[str, Any]\n    ) -> str | None:\n        while True:\n            usr_input = self.get_input_locale(key, perameters)\n            if usr_input is None:\n                return None\n            if usr_input == \"\":\n                return self.default\n            if usr_input == \" \":\n                continue\n            return usr_input\n\n    def get_input_locale(\n        self,\n        key: str,\n        perameters: dict[str, Any] | None = None,\n        escape: bool = True,\n    ) -> str | None:\n        if perameters is None:\n            perameters = {}\n        usr_input = color.ColoredInput().localize(key, escape, **perameters)\n        quit_key = core.core_data.local_manager.get_key(\"quit_key\")\n        if usr_input == \"\" or usr_input == quit_key:\n            return None\n        if usr_input == f\"\\\\{quit_key}\":\n            return quit_key\n        return usr_input\n\n\nclass StringEditor:\n    def __init__(self, item: str, value: str, item_localized: bool = False):\n        if item_localized:\n            item = core.core_data.local_manager.get_key(item)\n        self.item = item\n        self.value = value\n\n    def edit(self) -> str:\n        usr_input = StringInput(default=self.value).get_input_locale_while(\n            \"input_non_max\",\n            {\"name\": self.item, \"value\": self.value},\n        )\n        if usr_input is None:\n            return self.value\n        color.ColoredText.localize(\n            \"value_changed\",\n            name=self.item,\n            value=usr_input,\n        )\n        return usr_input\n\n\nclass YesNoInput:\n    def __init__(self, default: bool = False):\n        self.default = default\n\n    def get_input_once(\n        self, key: str, perameters: dict[str, Any] | None = None\n    ) -> bool | None:\n        if perameters is None:\n            perameters = {}\n        usr_input = color.ColoredInput().localize(key, **perameters)\n        if usr_input == \"\":\n            return self.default\n\n        if usr_input == core.core_data.local_manager.get_key(\"quit_key\"):\n            return None\n        return (\n            usr_input == core.core_data.local_manager.get_key(\"yes_key\")\n            or usr_input.lower().strip()\n            == core.core_data.local_manager.get_key(\"yes\").lower().strip()\n        )\n\n\nclass DialogBuilder:\n    def __init__(self, dialog_structure: dict[Any, Any]):\n        self.dialog_structure = dialog_structure\n"
  },
  {
    "path": "src/bcsfe/cli/edits/__init__.py",
    "content": "from bcsfe.cli.edits import (\n    basic_items,\n    cat_editor,\n    clear_tutorial,\n    rare_ticket_trade,\n    fixes,\n    enemy_editor,\n    aku_realm,\n    map,\n    event_tickets,\n    max_all,\n    storage,\n)\n\n__all__ = [\n    \"basic_items\",\n    \"cat_editor\",\n    \"clear_tutorial\",\n    \"rare_ticket_trade\",\n    \"fixes\",\n    \"enemy_editor\",\n    \"aku_realm\",\n    \"map\",\n    \"event_tickets\",\n    \"max_all\",\n    \"storage\",\n]\n"
  },
  {
    "path": "src/bcsfe/cli/edits/aku_realm.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\ndef unlock_aku_realm(save_file: core.SaveFile):\n    stage_ids = [255, 256, 257, 258, 265, 266, 268]\n    for stage_id in stage_ids:\n        save_file.event_stages.clear_map(1, stage_id, 0, False)\n\n    color.ColoredText.localize(\"aku_realm_unlocked\")\n"
  },
  {
    "path": "src/bcsfe/cli/edits/basic_items.py",
    "content": "from __future__ import annotations\nimport random\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color, edits\nfrom bcsfe.core.game.catbase.gatya_item import GatyaItemCategory\n\n\nclass BasicItems:\n    @staticmethod\n    def get_name(name: str | None, key: str) -> str:\n        if name is None:\n            return core.core_data.local_manager.get_key(key)\n        return name.strip()\n\n    @staticmethod\n    def reset_golden_cat_cpus(save_file: core.SaveFile):\n        save_file.golden_cpu_count = 0\n\n        color.ColoredText.localize(\"reset_golden_cat_cpus_success\")\n\n    @staticmethod\n    def edit_catfood(save_file: core.SaveFile):\n        should_exit = not dialog_creator.YesNoInput().get_input_once(\"catfood_warning\")\n        if should_exit:\n            return\n\n        name = core.core_data.get_gatya_item_names(save_file).get_name(22)\n        original_amount = save_file.catfood\n        save_file.catfood = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"catfood\"),\n            save_file.catfood,\n            core.core_data.max_value_manager.get(\"catfood\"),\n        ).edit()\n        change = save_file.catfood - original_amount\n        core.BackupMetaData(save_file).add_managed_item(\n            core.ManagedItem.from_change(change, core.ManagedItemType.CATFOOD)\n        )\n\n    @staticmethod\n    def edit_xp(save_file: core.SaveFile):\n        name = core.core_data.get_gatya_item_names(save_file).get_name(6)\n        save_file.xp = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"xp\"),\n            save_file.xp,\n            core.core_data.max_value_manager.get(\"xp\"),\n        ).edit()\n\n    @staticmethod\n    def edit_normal_tickets(save_file: core.SaveFile):\n        name = core.core_data.get_gatya_item_names(save_file).get_name(20)\n        save_file.normal_tickets = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"normal_tickets\"),\n            save_file.normal_tickets,\n            core.core_data.max_value_manager.get(\"normal_tickets\"),\n        ).edit()\n\n    @staticmethod\n    def edit_100_million_ticket(save_file: core.SaveFile):\n        color.ColoredText.localize(\"100_million_warn\")\n        name = core.core_data.get_gatya_item_names(save_file).get_name(212)\n        save_file.hundred_million_ticket = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"100_million_tickets\"),\n            save_file.hundred_million_ticket,\n            core.core_data.max_value_manager.get(\"100_million_tickets\"),\n        ).edit()\n\n    @staticmethod\n    def get_bannable_feature_options(feature_name: str, safe_feature_name: str) -> int:\n        feature_name = core.core_data.local_manager.get_key(feature_name)\n        safe_feature_name = core.core_data.local_manager.get_key(safe_feature_name)\n\n        options = [\n            core.core_data.local_manager.get_key(\n                \"continue_editing\", feature_name=feature_name\n            ),\n            core.core_data.local_manager.get_key(\n                \"go_to_safe_feature\", safer_feature_name=safe_feature_name\n            ),\n            core.core_data.local_manager.get_key(\n                \"cancel_editing\", feature_name=feature_name\n            ),\n        ]\n        option = dialog_creator.ChoiceInput(\n            options,\n            options,\n            [],\n            {\"feature_name\": feature_name},\n            \"select_an_option_to_continue\",\n        ).single_choice()\n        if option is None:\n            return 2\n        option -= 1\n        return option\n\n    @staticmethod\n    def edit_rare_tickets(save_file: core.SaveFile):\n        color.ColoredText.localize(\"rare_ticket_warning\")\n        name = core.core_data.get_gatya_item_names(save_file).get_name(21)\n        option = BasicItems.get_bannable_feature_options(\n            \"rare_tickets_l\", \"rare_ticket_trade_l\"\n        )\n        if option == 2:\n            return\n        if option == 1:\n            return edits.rare_ticket_trade.RareTicketTrade.rare_ticket_trade(save_file)\n\n        original_amount = save_file.rare_tickets\n        save_file.rare_tickets = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"rare_tickets\"),\n            save_file.rare_tickets,\n            core.core_data.max_value_manager.get(\"rare_tickets\"),\n        ).edit()\n        change = save_file.rare_tickets - original_amount\n        core.BackupMetaData(save_file).add_managed_item(\n            core.ManagedItem.from_change(change, core.ManagedItemType.RARE_TICKET)\n        )\n\n    @staticmethod\n    def edit_platinum_tickets(save_file: core.SaveFile):\n        color.ColoredText.localize(\"platinum_ticket_warning\")\n        name = core.core_data.get_gatya_item_names(save_file).get_name(29)\n        option = BasicItems.get_bannable_feature_options(\n            \"platinum_tickets_l\", \"platinum_shards_l\"\n        )\n        if option == 2:\n            return\n        if option == 1:\n            return edits.basic_items.BasicItems.edit_platinum_shards(save_file)\n\n        original_amount = save_file.platinum_tickets\n        save_file.platinum_tickets = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"platinum_tickets\"),\n            save_file.platinum_tickets,\n            core.core_data.max_value_manager.get(\"platinum_tickets\"),\n        ).edit()\n        change = save_file.platinum_tickets - original_amount\n        core.BackupMetaData(save_file).add_managed_item(\n            core.ManagedItem.from_change(change, core.ManagedItemType.PLATINUM_TICKET)\n        )\n\n    @staticmethod\n    def edit_legend_tickets(save_file: core.SaveFile):\n        should_exit = not dialog_creator.YesNoInput().get_input_once(\n            \"legend_ticket_warning\"\n        )\n        if should_exit:\n            return\n        name = core.core_data.get_gatya_item_names(save_file).get_name(145)\n        original_amount = save_file.legend_tickets\n        save_file.legend_tickets = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"legend_tickets\"),\n            save_file.legend_tickets,\n            core.core_data.max_value_manager.get(\"legend_tickets\"),\n        ).edit()\n        change = save_file.legend_tickets - original_amount\n        core.BackupMetaData(save_file).add_managed_item(\n            core.ManagedItem.from_change(change, core.ManagedItemType.LEGEND_TICKET)\n        )\n\n    @staticmethod\n    def edit_platinum_shards(save_file: core.SaveFile):\n        name = core.core_data.get_gatya_item_names(save_file).get_name(157)\n        platinum_ticket_amount = save_file.platinum_tickets\n        max_value = (\n            core.core_data.max_value_manager.get(\"platinum_tickets\")\n            - platinum_ticket_amount\n        ) * 10 + 9\n\n        max_value = max(0, max_value)\n        save_file.platinum_shards = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"platinum_shards\"),\n            save_file.platinum_shards,\n            max_value,\n        ).edit()\n\n    @staticmethod\n    def edit_np(save_file: core.SaveFile):\n        name = core.core_data.get_gatya_item_names(save_file).get_name(7)\n        save_file.np = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"np\"),\n            save_file.np,\n            core.core_data.max_value_manager.get(\"np\"),\n        ).edit()\n\n    @staticmethod\n    def edit_leadership(save_file: core.SaveFile):\n        name = core.core_data.get_gatya_item_names(save_file).get_name(105)\n        save_file.leadership = dialog_creator.SingleEditor(\n            BasicItems.get_name(name, \"leadership\"),\n            save_file.leadership,\n            core.core_data.max_value_manager.get(\"leadership\"),\n        ).edit()\n\n    @staticmethod\n    def edit_battle_items(save_file: core.SaveFile):\n        save_file.battle_items.edit(save_file)\n\n    @staticmethod\n    def edit_battle_items_endless(save_file: core.SaveFile):\n        save_file.battle_items.edit_endless_items(save_file)\n\n    @staticmethod\n    def edit_catamins(save_file: core.SaveFile):\n        names_o = core.core_data.get_gatya_item_names(save_file)\n        items = core.core_data.get_gatya_item_buy(save_file).get_by_category(6)\n        if items is None:\n            return\n        names: list[str] = []\n        for item in items:\n            name = names_o.get_name(item.id)\n            if name is None:\n                name = core.core_data.local_manager.get_key(\n                    \"unknown_catamin_name\", id=item.id\n                )\n            names.append(name)\n        values = dialog_creator.MultiEditor.from_reduced(\n            \"catamins\",\n            names,\n            save_file.catamins,\n            core.core_data.max_value_manager.get(\"catamins\"),\n            group_name_localized=True,\n        ).edit()\n        save_file.catamins = values\n\n    @staticmethod\n    def edit_catseyes(save_file: core.SaveFile):\n        names_o = core.core_data.get_gatya_item_names(save_file)\n        items = core.core_data.get_gatya_item_buy(save_file).get_by_category(5)\n        if items is None:\n            return\n        names: list[str] = []\n        for item in items:\n            name = names_o.get_name(item.id)\n            if name is None:\n                name = core.core_data.local_manager.get_key(\n                    \"unknown_catseye_name\", id=item.id\n                )\n            names.append(name)\n\n        values = dialog_creator.MultiEditor.from_reduced(\n            \"catseyes\",\n            names,\n            save_file.catseyes,\n            core.core_data.max_value_manager.get(\"catseyes\"),\n            group_name_localized=True,\n        ).edit()\n        save_file.catseyes = values\n\n    @staticmethod\n    def edit_treasure_chests(save_file: core.SaveFile):\n        names_o = core.core_data.get_gatya_item_names(save_file)\n        items = core.core_data.get_gatya_item_buy(save_file).get_by_category(\n            GatyaItemCategory.TREASURE_CHESTS\n        )\n        if items is None:\n            return\n        names: list[str] = []\n        for item in items[: len(save_file.treasure_chests)]:\n            name = names_o.get_name(item.id)\n            if name is None:\n                name = core.core_data.local_manager.get_key(\n                    \"unknown_treasure_chest_name\", id=item.id\n                )\n            names.append(name)\n\n        values = dialog_creator.MultiEditor.from_reduced(\n            \"treasure_chests\",\n            names,\n            save_file.treasure_chests,\n            core.core_data.max_value_manager.get(\"treasure_chests\"),\n            group_name_localized=True,\n        ).edit()\n        save_file.treasure_chests = values\n\n    @staticmethod\n    def edit_catfruit(save_file: core.SaveFile):\n        names = core.Matatabi(save_file).get_names()\n        if names is None:\n            return\n        new_names: list[str] = []\n        for i, name in enumerate(names):\n            if name is None:\n                name = core.core_data.local_manager.get_key(\n                    \"unknown_catfruit_name\", id=i\n                )\n            new_names.append(name)\n        names = new_names\n\n        extra = len(save_file.catfruit) - len(names)\n        if extra > 0:\n            for i in range(extra):\n                names.append(\n                    core.core_data.local_manager.get_key(\n                        \"unknown_catfruit_name\", id=i + len(names)\n                    )\n                )\n\n        if save_file.game_version < 110400:\n            max_value = core.core_data.max_value_manager.get_old(\"catfruit\")\n            cumulative_max = True\n        else:\n            max_value = core.core_data.max_value_manager.get_new(\"catfruit\")\n            cumulative_max = False\n\n        names = names[: len(save_file.catfruit)]\n\n        values = dialog_creator.MultiEditor.from_reduced(\n            \"catfruit\",\n            names,\n            save_file.catfruit,\n            max_value,\n            group_name_localized=True,\n            cumulative_max=cumulative_max,\n        ).edit()\n        save_file.catfruit = values\n\n    @staticmethod\n    def set_restart_pack(save_file: core.SaveFile):\n        save_file.restart_pack = 1\n        name = core.core_data.get_gatya_item_names(save_file).get_name(123)\n        color.ColoredText.localize(\"value_gave\", name=name)\n\n    @staticmethod\n    def edit_inquiry_code(save_file: core.SaveFile):\n        should_exit = not dialog_creator.YesNoInput().get_input_once(\n            \"inquiry_code_warning\"\n        )\n        if should_exit:\n            return\n        item_name = save_file.get_localizable().get(\"autoSave_txt5\")\n        save_file.inquiry_code = dialog_creator.StringEditor(\n            BasicItems.get_name(item_name, \"inquiry_code\"),\n            save_file.inquiry_code,\n        ).edit()\n\n    @staticmethod\n    def edit_password_refresh_token(save_file: core.SaveFile):\n        should_exit = not dialog_creator.YesNoInput().get_input_once(\n            \"password_refresh_token_warning\"\n        )\n        if should_exit:\n            return\n        save_file.password_refresh_token = dialog_creator.StringEditor(\n            \"password_refresh_token\",\n            save_file.password_refresh_token,\n            item_localized=True,\n        ).edit()\n\n    @staticmethod\n    def edit_scheme_items(save_file: core.SaveFile):\n        save_file.scheme_items.edit(save_file)\n\n    @staticmethod\n    def edit_engineers(save_file: core.SaveFile):\n        save_file.ototo.edit_engineers(save_file)\n\n    @staticmethod\n    def edit_base_materials(save_file: core.SaveFile):\n        save_file.ototo.base_materials.edit_base_materials(save_file)\n\n    @staticmethod\n    def edit_rare_gatya_seed(save_file: core.SaveFile):\n        save_file.gatya.edit_rare_gatya_seed()\n\n    @staticmethod\n    def edit_normal_gatya_seed(save_file: core.SaveFile):\n        save_file.gatya.edit_normal_gatya_seed()\n\n    @staticmethod\n    def edit_event_gatya_seed(save_file: core.SaveFile):\n        save_file.gatya.edit_event_gatya_seed()\n\n    @staticmethod\n    def edit_unlocked_slots(save_file: core.SaveFile):\n        save_file.lineups.edit_unlocked_slots()\n\n    @staticmethod\n    def edit_labyrinth_medals(save_file: core.SaveFile):\n        names_o = core.core_data.get_gatya_item_names(save_file)\n        items = core.core_data.get_gatya_item_buy(save_file).get_by_category(11)\n        if items is None:\n            return\n        names: list[str] = []\n        for item in items:\n            name = names_o.get_name(item.id)\n            if name is None:\n                name = core.core_data.local_manager.get_key(\n                    \"unknown_labyrinth_medal_name\", id=item.id\n                )\n            names.append(name)\n\n        values = dialog_creator.MultiEditor.from_reduced(\n            \"labyrinth_medals\",\n            names,\n            save_file.labyrinth_medals,\n            core.core_data.max_value_manager.get(\"labyrinth_medals\"),\n            group_name_localized=True,\n        ).edit()\n        save_file.labyrinth_medals = values\n\n    @staticmethod\n    def edit_special_skills(save_file: core.SaveFile):\n        save_file.special_skills.edit(save_file)\n\n    @staticmethod\n    def unlock_equip_menu(save_file: core.SaveFile):\n        save_file.unlock_equip_menu()\n        color.ColoredText.localize(\"equip_menu_unlocked\")\n\n    @staticmethod\n    def allow_filibuster_stage_reclearing(save_file: core.SaveFile):\n        save_file.filibuster_stage_enabled = True\n        save_file.filibuster_stage_id = random.randint(0, 47)\n        color.ColoredText.localize(\"filibuster_stage_reclearing_allowed\")\n"
  },
  {
    "path": "src/bcsfe/cli/edits/cat_editor.py",
    "content": "from __future__ import annotations\nimport enum\nfrom typing import Any, Callable\n\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass SelectMode(enum.Enum):\n    AND = 0\n    OR = 1\n    REPLACE = 2\n\n\nclass CatEditor:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n\n    def get_current_cats(self):\n        return self.save_file.cats.get_unlocked_cats()\n\n    def get_non_unlocked_cats(self):\n        return self.save_file.cats.get_non_unlocked_cats()\n\n    def get_non_gacha_cats(self):\n        return self.save_file.cats.get_non_gacha_cats(self.save_file)\n\n    def filter_cats(self, cats: list[core.Cat]) -> list[core.Cat]:\n        unlocked_cats = self.get_current_cats()\n        return [cat for cat in cats if cat in unlocked_cats]\n\n    def get_cats_rarity(self, rarity: int) -> list[core.Cat]:\n        return self.save_file.cats.get_cats_rarity(self.save_file, rarity)\n\n    def get_cats_name(self, name: str) -> list[core.Cat]:\n        return self.save_file.cats.get_cats_name(self.save_file, name)\n\n    def get_cats_obtainable(self) -> list[core.Cat] | None:\n        return self.save_file.cats.get_cats_obtainable(self.save_file)\n\n    def get_cats_unobtainable(self) -> list[core.Cat] | None:\n        return self.save_file.cats.get_cats_non_obtainable(self.save_file)\n\n    def get_cats_gatya_banner(self, gatya_id: int) -> list[core.Cat] | None:\n        return self.save_file.cats.get_cats_gatya_banner(self.save_file, gatya_id)\n\n    def print_selected_cats(self, current_cats: list[core.Cat]):\n        if len(current_cats) > 50:\n            color.ColoredText.localize(\"total_selected_cats\", total=len(current_cats))\n        else:\n            for cat in current_cats:\n                names = cat.get_names_cls(self.save_file)\n                if not names:\n                    names = [str(cat.id)]\n                color.ColoredText.localize(\"selected_cat\", id=cat.id, name=names[0])\n\n    def select(\n        self, current_cats: list[core.Cat] | None = None, finish_option: bool = True\n    ) -> tuple[list[core.Cat], bool]:\n        if current_cats is None:\n            current_cats = []\n        options: dict[str, Callable[[], Any]] = {\n            \"select_cats_all\": self.save_file.cats.get_all_cats,\n            \"select_cats_current\": self.get_current_cats,\n            \"select_cats_obtainable\": self.get_cats_obtainable,\n            \"select_cats_id\": self.select_id,\n            \"select_cats_name\": self.select_name,\n            \"select_cats_rarity\": self.select_rarity,\n            \"select_cats_gatya_banner\": self.select_gatya_banner,\n            \"select_cats_not_unlocked\": self.get_non_unlocked_cats,\n            \"select_cats_not_obtainable\": self.get_cats_unobtainable,\n            \"select_cats_non_gatya\": self.get_non_gacha_cats,\n            \"select_cats_game_version\": self.select_cats_game_version,\n        }\n        if finish_option:\n            options[\"finish\"] = lambda: None\n        option_id = dialog_creator.ChoiceInput(\n            list(options), list(options), [], {}, \"select_cats\", True\n        ).single_choice()\n        if option_id is None:\n            return current_cats, False\n        option_id -= 1\n\n        if option_id == len(options) - 1 and finish_option:\n            return current_cats, True\n\n        func = options[list(options)[option_id]]\n        new_cats = func()\n\n        if new_cats is None:\n            return current_cats, False\n\n        if current_cats:\n            mode_id = dialog_creator.IntInput().get_basic_input_locale(\"and_mode_q\", {})\n            if mode_id is None:\n                mode = SelectMode.OR\n            elif mode_id == 1:\n                mode = SelectMode.AND\n            elif mode_id == 2:\n                mode = SelectMode.OR\n            elif mode_id == 3:\n                mode = SelectMode.REPLACE\n            else:\n                mode = SelectMode.OR\n        else:\n            mode = SelectMode.OR\n\n        if mode == SelectMode.AND:\n            return list(set(current_cats) & set(new_cats)), False\n        if mode == SelectMode.OR:\n            return list(set(current_cats) | set(new_cats)), False\n        if mode == SelectMode.REPLACE:\n            return new_cats, False\n        return new_cats, False\n\n    def select_id(self) -> list[core.Cat] | None:\n        cat_ids = dialog_creator.RangeInput(\n            len(self.save_file.cats.cats) - 1\n        ).get_input_locale(\"enter_cat_ids\", {})\n        if cat_ids is None:\n            return None\n        return self.save_file.cats.get_cats_by_ids(cat_ids)\n\n    def select_cats_game_version(self) -> list[core.Cat] | None:\n        unitbuy = core.UnitBuy(self.save_file)\n        if unitbuy.unit_buy is None:\n            return None\n\n        versions_set: set[int] = set()\n        for cat in unitbuy.unit_buy:\n            if cat.game_version == -1:\n                continue\n            versions_set.add(cat.game_version)\n\n        if not versions_set:\n            return None\n\n        versions = list(versions_set)\n        versions.sort()\n\n        color.ColoredText.localize(\"possible_gvs\")\n\n        cur_major_v = -1\n        for version in versions:\n            gv = core.GameVersion(version)\n            major_v = gv.get_parts()[0]\n            if major_v != cur_major_v:\n                if cur_major_v != -1:\n                    print()\n                cur_major_v = major_v\n            else:\n                color.ColoredText(\", \", end=\"\")\n            color.ColoredText(f\"<@t>{gv.format()}</>\", end=\"\")\n\n        print()\n\n        usr_input = dialog_creator.StringInput().get_input_locale(\"select_gv\")\n        if usr_input is None:\n            return None\n        chunks = usr_input.split(\" \")\n\n        versions_selected: list[int] = []\n        for chunk in chunks:\n            parts = chunk.split(\"-\")\n            if len(parts) == 2:\n                min = parts[0]\n                max = parts[1]\n\n                v1 = core.GameVersion.from_string(min)\n                v2 = core.GameVersion.from_string(max)\n\n                for v in range(v1.game_version, v2.game_version + 1):\n                    versions_selected.append(v)\n            else:\n                v = core.GameVersion.from_string(chunk)\n                versions_selected.append(v.game_version)\n\n        valid_versions: set[int] = set()\n        for version in versions_selected:\n            if version in versions_set:\n                valid_versions.add(version)\n\n        if not valid_versions:\n            color.ColoredText.localize(\"no_valid_gvs_entered\")\n\n        cats: list[core.Cat] = []\n        for cat in self.save_file.cats.cats:\n            row = unitbuy.get_unit_buy(cat.id)\n            if row is None:\n                continue\n            if row.game_version in valid_versions:\n                cats.append(cat)\n\n        return cats\n\n    def select_rarity(self) -> list[core.Cat] | None:\n        rarity_names = self.save_file.cats.get_rarity_names(self.save_file)\n        rarity_ids, _ = dialog_creator.ChoiceInput(\n            rarity_names, rarity_names, [], {}, \"select_rarity\"\n        ).multiple_choice()\n        if rarity_ids is None:\n            return None\n        cats: list[core.Cat] = []\n        for rarity_id in rarity_ids:\n            rarity_cats = self.get_cats_rarity(rarity_id)\n            cats = list(set(cats + rarity_cats))\n        return cats\n\n    def select_name(self) -> list[core.Cat] | None:\n        usr_name = dialog_creator.StringInput().get_input_locale(\"enter_name\", {})\n        if usr_name is None:\n            return []\n        cats = self.get_cats_name(usr_name)\n        if not cats:\n            color.ColoredText.localize(\"no_cats_found_name\", name=usr_name)\n            return None\n        cat_names: list[str] = []\n        cat_list: list[core.Cat] = []\n        for cat in cats:\n            names = cat.get_names_cls(self.save_file)\n            if not names:\n                names = [str(cat.id)]\n            for name in names:\n                if usr_name.lower() in name.lower():\n                    cat_names.append(name)\n                    cat_list.append(cat)\n                    break\n        if len(cat_names) == 1:\n            color.ColoredText(f\"<@t>{cat_names[0]}</>\")\n        cat_option_ids, _ = dialog_creator.ChoiceInput(\n            cat_names, cat_names, [], {}, \"select_name\"\n        ).multiple_choice()\n        if cat_option_ids is None:\n            return None\n        cats_selected: list[core.Cat] = []\n        for cat_option_id in cat_option_ids:\n            cats_selected.append(cat_list[cat_option_id])\n        return cats_selected\n\n    def select_obtainable(self) -> list[core.Cat] | None:\n        return self.get_cats_obtainable()\n\n    def select_gatya_banner_name(self) -> list[int] | None:\n\n        filter_down = dialog_creator.YesNoInput().get_input_once(\"filter_down_q_gatya\")\n        if filter_down is None:\n            return None\n\n        all_names = core.GatyaInfos(self.save_file).get_all_names()\n        ids = list(all_names.keys())\n        ids.sort()\n        names: list[str] = []\n        for id in ids:\n            names.append(all_names[id])\n        new_names: list[str] = []\n        new_ids: list[int] = []\n\n        unknown_name = core.core_data.local_manager.get_key(\"unknown_banner\")\n\n        if filter_down:\n            ids.reverse()\n            for id in ids:\n                name = all_names[id]\n                if name in new_names or name == unknown_name:\n                    continue\n                new_names.append(name)\n                new_ids.append(id)\n            new_ids.reverse()\n            new_names.reverse()\n        else:\n            new_names = names\n            new_ids = ids\n\n        ids = new_ids\n\n        formatted_names: list[str] = []\n\n        for name in new_names:\n            formatted_name = core.core_data.local_manager.get_key(\n                \"banner_txt\", name=name\n            )\n            formatted_names.append(formatted_name)\n        gatya_option_ids, _ = dialog_creator.ChoiceInput.from_reduced(\n            formatted_names,\n            ints=ids,\n            dialog=\"select_gatya_banner\",\n            start_index=0,\n        ).multiple_choice(False)\n        if gatya_option_ids is None:\n            return None\n        gatya_ids: list[int] = []\n        for gatya_option_id in gatya_option_ids:\n            gatya_ids.append(ids[gatya_option_id])\n\n        return gatya_ids\n\n    def select_gatya_banner(self) -> list[core.Cat] | None:\n        gset = self.save_file.gatya.read_gatya_data_set(self.save_file).gatya_data_set\n        if gset is None:\n            return None\n\n        by_id = dialog_creator.ChoiceInput.from_reduced(\n            [\"by_id\", \"by_name\"], dialog=\"gatya_by_id_q\"\n        ).single_choice()\n        if by_id is None:\n            return None\n\n        if by_id == 1:\n            gatya_ids = dialog_creator.RangeInput(len(gset) - 1).get_input_locale(\n                \"select_gatya_banner\", {}\n            )\n        else:\n            gatya_ids = self.select_gatya_banner_name()\n        if gatya_ids is None:\n            return None\n        cats: list[core.Cat] = []\n        for gatya_id in gatya_ids:\n            gatya_cats = self.get_cats_gatya_banner(gatya_id)\n            if gatya_cats is None:\n                continue\n            cats = list(set(cats + gatya_cats))\n        return cats\n\n    def unlock_cats(self, cats: list[core.Cat]):\n        cats = self.get_save_cats(cats)\n        for cat in cats:\n            cat.unlock(self.save_file)\n        color.ColoredText.localize(\"unlock_success\")\n\n    def remove_cats(self, cats: list[core.Cat]):\n        reset = core.core_data.config.get_bool(core.ConfigKey.RESET_CAT_DATA)\n        cats = self.get_save_cats(cats)\n        for cat in cats:\n            cat.remove(reset=reset, save_file=self.save_file)\n        color.ColoredText.localize(\"remove_success\")\n\n    def get_save_cats(self, cats: list[core.Cat]):\n        ct_cats: list[core.Cat] = []\n        for cat in cats:\n            ct = self.save_file.cats.get_cat_by_id(cat.id)\n            if ct is None:\n                continue\n            ct_cats.append(ct)\n        return ct_cats\n\n    def true_form_cats(self, cats: list[core.Cat], force: bool = False):\n        cats = self.get_save_cats(cats)\n        set_current_forms = core.core_data.config.get_bool(\n            core.ConfigKey.SET_CAT_CURRENT_FORMS\n        )\n        self.save_file.cats.true_form_cats(\n            self.save_file, cats, force, set_current_forms\n        )\n        color.ColoredText.localize(\"true_form_success\")\n\n    def fourth_form_cats(self, cats: list[core.Cat], force: bool = False):\n        cats = self.get_save_cats(cats)\n        set_current_forms = core.core_data.config.get_bool(\n            core.ConfigKey.SET_CAT_CURRENT_FORMS\n        )\n        self.save_file.cats.fourth_form_cats(\n            self.save_file, cats, force, set_current_forms\n        )\n        color.ColoredText.localize(\"fourth_form_success\")\n\n    def remove_true_form_cats(self, cats: list[core.Cat]):\n        cats = self.get_save_cats(cats)\n        for cat in cats:\n            cat.remove_true_form()\n        color.ColoredText.localize(\"remove_true_form_success\")\n\n    def remove_fourth_form_cats(self, cats: list[core.Cat]):\n        cats = self.get_save_cats(cats)\n        for cat in cats:\n            cat.remove_fourth_form()\n        color.ColoredText.localize(\"remove_fourth_form_success\")\n\n    def upgrade_cats(self, cats: list[core.Cat]):\n        cats = self.get_save_cats(cats)\n        if not cats:\n            return\n        if len(cats) == 1:\n            option_id = 0\n        else:\n            options: list[str] = [\n                \"upgrade_individual\",\n                \"upgrade_all\",\n            ]\n            option_id = dialog_creator.ChoiceInput(\n                options, options, [], {}, \"upgrade_cats_select_mod\", True\n            ).single_choice()\n            if option_id is None:\n                return\n            option_id -= 1\n        success = False\n        if option_id == 0:\n            for cat in cats:\n                names = cat.get_names_cls(self.save_file)\n                if not names:\n                    names = [str(cat.id)]\n                color.ColoredText.localize(\n                    \"selected_cat_upgrades\",\n                    name=names[0],\n                    id=cat.id,\n                    base_level=cat.upgrade.base + 1,\n                    plus_level=cat.upgrade.plus,\n                )\n                power_up = core.PowerUpHelper(cat, self.save_file)\n                upgrade, should_exit = core.Upgrade.get_user_upgrade(\n                    power_up.get_max_possible_base() - 1,\n                    power_up.get_max_possible_plus(),\n                )\n                if should_exit:\n                    return\n                if upgrade is not None:\n                    power_up.reset_upgrade()\n                    power_up.upgrade_by(upgrade.base)\n                    cat.set_upgrade(self.save_file, upgrade, True)\n                    color.ColoredText.localize(\n                        \"selected_cat_upgraded\",\n                        name=names[0],\n                        id=cat.id,\n                        base_level=cat.upgrade.base + 1,\n                        plus_level=cat.upgrade.plus,\n                    )\n                    success = True\n        else:\n            power_up = core.PowerUpHelper(cats[0], self.save_file)\n            upgrade, should_exit = core.Upgrade.get_user_upgrade(\n                power_up.get_max_max_base_upgrade_level() - 1,\n                power_up.get_max_max_plus_upgrade_level(),\n            )\n            if upgrade is None or should_exit:\n                return\n            success = True\n            for cat in cats:\n                power_up = core.PowerUpHelper(cat, self.save_file)\n                power_up.reset_upgrade()\n                power_up.upgrade_by(upgrade.base)\n                cat.set_upgrade(self.save_file, upgrade, True)\n        if success:\n            color.ColoredText.localize(\"upgrade_success\")\n\n    def remove_talents_cats(self, cats: list[core.Cat]):\n        for cat in cats:\n            if cat.talents is None:\n                continue\n            for talent in cat.talents:\n                talent.level = 0\n        color.ColoredText.localize(\"talents_remove_success\")\n\n    def unlock_cat_guide(self, cats: list[core.Cat]):\n        for cat in cats:\n            if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT):\n                cat.unlock(self.save_file)\n            cat.catguide_collected = True\n        color.ColoredText.localize(\"unlock_cat_guide_success\")\n\n    def remove_cat_guide(self, cats: list[core.Cat]):\n        for cat in cats:\n            cat.catguide_collected = False\n        color.ColoredText.localize(\"remove_cat_guide_success\")\n\n    def upgrade_talents_cats(self, cats: list[core.Cat]):\n        cats = self.get_save_cats(cats)\n        if not cats:\n            return\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        is_good_version = gdg.does_save_version_match(self.save_file)\n        if not is_good_version:\n            data_version = gdg.version\n            if data_version is None:\n                color.ColoredText.localize(\"no_data_version\")\n                return\n            color.ColoredText.localize(\n                \"talents_version_warning\",\n                save_version=self.save_file.game_version.to_string(),\n                data_version=data_version,\n            )\n            should_stay = dialog_creator.YesNoInput().get_input_once(\"continue_q\")\n            if not should_stay:\n                return\n\n        if len(cats) == 1:\n            option_id = 0\n        else:\n            options: list[str] = [\n                \"talents_individual\",\n                \"talents_all\",\n            ]\n            option_id = dialog_creator.ChoiceInput(\n                options, options, [], {}, \"upgrade_talents_select_mod\", True\n            ).single_choice()\n            if option_id is None:\n                return\n            option_id -= 1\n\n        talent_data = self.save_file.cats.read_talent_data(self.save_file)\n        if talent_data is None:\n            return\n        if option_id == 0:\n            for cat in cats:\n                if cat.talents is None:\n                    continue\n                names = cat.get_names_cls(self.save_file)\n                if not names:\n                    names = [str(cat.id)]\n                color.ColoredText.localize(\n                    \"selected_cat\",\n                    name=names[0],\n                    id=cat.id,\n                )\n                data = talent_data.get_cat_talents(cat)\n                if data is None:\n                    color.ColoredText.localize(\"no_talent_data\", id=cat.id)\n                    continue\n                if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT):\n                    cat.unlock(self.save_file)\n                talent_names, max_levels, current_levels, ids = data\n                values = dialog_creator.MultiEditor.from_reduced(\n                    \"talents\",\n                    talent_names,\n                    current_levels,\n                    max_levels,\n                    group_name_localized=True,\n                ).edit()\n                current_levels = values\n                for i, id in enumerate(ids):\n                    talent = cat.get_talent_from_id(id)\n                    if talent is None:\n                        continue\n                    talent.level = current_levels[i]\n        else:\n            for cat in cats:\n                if cat.talents is None:\n                    continue\n                data = talent_data.get_cat_talents(cat)\n                if data is None:\n                    continue\n                if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT):\n                    cat.unlock(self.save_file)\n                talent_names, max_levels, current_levels, ids = data\n                for i, id in enumerate(ids):\n                    talent = cat.get_talent_from_id(id)\n                    if talent is None:\n                        continue\n                    talent.level = max_levels[i]\n        color.ColoredText.localize(\"talents_success\")\n\n    @staticmethod\n    def edit_cats(save_file: core.SaveFile):\n        cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        while True:\n            should_exit, current_cats = cat_editor.run_edit_cats(current_cats)\n            if should_exit:\n                break\n\n    @staticmethod\n    def unlock_remove_cats_run(\n        save_file: core.SaveFile,\n        current_cats: list[core.Cat] | None = None,\n        cat_editor: CatEditor | None = None,\n    ):\n        if cat_editor is None or current_cats is None:\n            cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        choice = dialog_creator.ChoiceInput(\n            [\"unlock_cats\", \"remove_cats\"],\n            [\"unlock_cats\", \"remove_cats\"],\n            [],\n            {},\n            \"unlock_remove_q\",\n            True,\n            remove_alias=True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        if choice == 0:\n            cat_editor.unlock_cats(current_cats)\n        elif choice == 1:\n            cat_editor.remove_cats(current_cats)\n        CatEditor.set_rank_up_sale(save_file)\n\n    @staticmethod\n    def true_form_remove_form_cats_run(\n        save_file: core.SaveFile,\n        current_cats: list[core.Cat] | None = None,\n        cat_editor: CatEditor | None = None,\n    ):\n        if cat_editor is None or current_cats is None:\n            cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            [\"true_form_cats\", \"remove_true_form_cats\"],\n            dialog=\"true_form_remove_form_q\",\n            single_choice=True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        if choice == 0:\n            cat_editor.true_form_cats(current_cats)\n        elif choice == 1:\n            cat_editor.remove_true_form_cats(current_cats)\n\n    @staticmethod\n    def fourth_form_remove_form_cats_run(\n        save_file: core.SaveFile,\n        current_cats: list[core.Cat] | None = None,\n        cat_editor: CatEditor | None = None,\n    ):\n        if cat_editor is None or current_cats is None:\n            cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            [\"fourth_form_cats\", \"remove_fourth_form_cats\"],\n            dialog=\"fourth_form_remove_form_q\",\n            single_choice=True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        if choice == 0:\n            cat_editor.fourth_form_cats(current_cats)\n        elif choice == 1:\n            cat_editor.remove_fourth_form_cats(current_cats)\n\n    @staticmethod\n    def force_true_form_cats_run(save_file: core.SaveFile):\n        color.ColoredText.localize(\"force_true_form_cats_warning\")\n        cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        cat_editor.true_form_cats(current_cats, force=True)\n\n    @staticmethod\n    def force_fourth_form_cats_run(save_file: core.SaveFile):\n        color.ColoredText.localize(\"force_fourth_form_cats_warning\")\n        cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        cat_editor.fourth_form_cats(current_cats, force=True)\n\n    @staticmethod\n    def upgrade_cats_run(save_file: core.SaveFile):\n        cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        cat_editor.upgrade_cats(current_cats)\n        CatEditor.set_rank_up_sale(save_file)\n\n    @staticmethod\n    def upgrade_talents_remove_talents_cats_run(\n        save_file: core.SaveFile,\n        current_cats: list[core.Cat] | None = None,\n        cat_editor: CatEditor | None = None,\n    ):\n        if cat_editor is None or current_cats is None:\n            cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        choice = dialog_creator.ChoiceInput(\n            [\"upgrade_talents_cats\", \"remove_talents_cats\"],\n            [\"upgrade_talents_cats\", \"remove_talents_cats\"],\n            [],\n            {},\n            \"upgrade_talents_remove_talents_q\",\n            True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        if choice == 0:\n            cat_editor.upgrade_talents_cats(current_cats)\n        elif choice == 1:\n            cat_editor.remove_talents_cats(current_cats)\n\n    @staticmethod\n    def unlock_cat_guide_remove_guide_run(\n        save_file: core.SaveFile,\n        current_cats: list[core.Cat] | None = None,\n        cat_editor: CatEditor | None = None,\n    ):\n        if cat_editor is None or current_cats is None:\n            cat_editor, current_cats = CatEditor.from_save_file(save_file)\n        if cat_editor is None:\n            return\n        choice = dialog_creator.ChoiceInput(\n            [\"unlock_cat_guide\", \"remove_cat_guide\"],\n            [\"unlock_cat_guide\", \"remove_cat_guide\"],\n            [],\n            {},\n            \"unlock_cat_guide_remove_guide_q\",\n            True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        if choice == 0:\n            cat_editor.unlock_cat_guide(current_cats)\n        elif choice == 1:\n            cat_editor.remove_cat_guide(current_cats)\n\n    @staticmethod\n    def from_save_file(\n        save_file: core.SaveFile,\n    ) -> tuple[CatEditor | None, list[core.Cat]]:\n        cat_editor = CatEditor(save_file)\n        stop = False\n        cats = []\n        while not stop:\n            current_cats, finished = cat_editor.select(cats)\n            cats = current_cats\n            cat_editor.print_selected_cats(cats)\n            if finished:\n                stop = True\n                continue\n            finished = dialog_creator.YesNoInput().get_input_once(\n                \"finished_cats_selection\"\n            )\n            if finished is None:\n                return None, []\n            stop = finished\n        return cat_editor, cats\n\n    def run_edit_cats(\n        self,\n        cats: list[core.Cat],\n    ) -> tuple[bool, list[core.Cat]]:\n        self.print_selected_cats(cats)\n        options: list[str] = [\n            \"select_cats_again\",\n            \"unlock_remove_cats\",\n            \"upgrade_cats\",\n            \"true_form_remove_form_cats\",\n            \"force_true_form_cats\",\n            \"fourth_form_remove_form_cats\",\n            \"force_fourth_form_cats\",\n            \"upgrade_talents_remove_talents_cats\",\n            \"unlock_remove_cat_guide\",\n            \"finish_edit_cats\",\n        ]\n        option_id = dialog_creator.ChoiceInput(\n            options,\n            options,\n            [],\n            {},\n            \"select_edit_cats_option\",\n            True,\n            remove_alias=True,\n        ).single_choice()\n        if option_id is None:\n            return False, cats\n        option_id -= 1\n        if option_id == 0:\n            cats_, _ = self.select(cats, False)\n            cats = cats_\n        elif option_id == 1:\n            self.unlock_remove_cats_run(self.save_file, cats, self)\n        elif option_id == 2:\n            self.upgrade_cats(cats)\n        elif option_id == 3:\n            self.true_form_remove_form_cats_run(self.save_file, cats, self)\n        elif option_id == 4:\n            color.ColoredText.localize(\"force_true_form_cats_warning\")\n            self.true_form_cats(cats, force=True)\n        elif option_id == 5:\n            self.fourth_form_remove_form_cats_run(self.save_file, cats, self)\n        elif option_id == 6:\n            color.ColoredText.localize(\"force_fourth_form_cats_warning\")\n            self.fourth_form_cats(cats, force=True)\n        elif option_id == 7:\n            self.upgrade_talents_remove_talents_cats_run(self.save_file, cats, self)\n        elif option_id == 8:\n            self.unlock_cat_guide_remove_guide_run(self.save_file, cats, self)\n        CatEditor.set_rank_up_sale(self.save_file)\n        if option_id == 9:\n            return True, cats\n        return False, cats\n\n    @staticmethod\n    def set_rank_up_sale(save_file: core.SaveFile):\n        save_file.rank_up_sale_value = 0x7FFFFFFF\n"
  },
  {
    "path": "src/bcsfe/cli/edits/clear_tutorial.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\ndef clear_tutorial(\n    save_file: core.SaveFile, display_already_cleared: bool = True\n):\n    core.StoryChapters.clear_tutorial(save_file)\n    if display_already_cleared:\n        color.ColoredText.localize(\"tutorial_cleared\")\n"
  },
  {
    "path": "src/bcsfe/cli/edits/enemy_editor.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\nfrom bcsfe.cli.edits.cat_editor import SelectMode\nfrom bcsfe.core.game.battle.enemy import EnemyNames\n\n\nclass EnemyEditor:\n    def __init__(self, save_file: core.SaveFile) -> None:\n        self.save_file = save_file\n\n    def unlock_enemy_guide(self, enemies: list[core.Enemy]):\n        for enemy in enemies:\n            enemy.unlock_enemy_guide(self.save_file)\n\n        color.ColoredText.localize(\"unlock_enemy_guide_success\")\n\n    def remove_enemy_guide(self, enemies: list[core.Enemy]):\n        for enemy in enemies:\n            enemy.reset_enemy_guide(self.save_file)\n\n        color.ColoredText.localize(\"remove_enemy_guide_success\")\n\n    def print_selected_enemies(self, enemies: list[core.Enemy]):\n        if not enemies:\n            return\n        if len(enemies) > 50:\n            color.ColoredText.localize(\"total_selected_enemies\", total=len(enemies))\n        else:\n            for enemy in enemies:\n                color.ColoredText.localize(\n                    \"selected_enemy\",\n                    id=enemy.id,\n                    name=enemy.get_name(self.save_file),\n                )\n\n    def select(self, current_enemies: list[core.Enemy] | None):\n        if current_enemies is None:\n            current_enemies = []\n        self.print_selected_enemies(current_enemies)\n\n        options: dict[str, Any] = {\n            \"select_enemies_valid\": self.get_all_valid_enemies,\n            \"select_enemies_all\": self.get_all_enemies,\n            \"select_enemies_id\": self.select_id,\n            \"select_enemies_name\": self.select_name,\n            \"select_enemies_invalid\": self.get_all_invalid_enemies,\n        }\n        option_id = dialog_creator.ChoiceInput.from_reduced(\n            list(options), dialog=\"select_enemies\", single_choice=True\n        ).single_choice()\n        if option_id is None:\n            return current_enemies\n        option_id -= 1\n\n        func = options[list(options)[option_id]]\n        new_enemies = func()\n        if new_enemies is None:\n            return None\n\n        if current_enemies:\n            mode_id = dialog_creator.IntInput().get_basic_input_locale(\"and_mode_q\", {})\n            if mode_id is None:\n                mode = SelectMode.OR\n            elif mode_id == 1:\n                mode = SelectMode.AND\n            elif mode_id == 2:\n                mode = SelectMode.OR\n            elif mode_id == 3:\n                mode = SelectMode.REPLACE\n            else:\n                mode = SelectMode.OR\n        else:\n            mode = SelectMode.OR\n\n        if mode == SelectMode.AND:\n            return [enemy for enemy in new_enemies if enemy in current_enemies]\n        if mode == SelectMode.OR:\n            return list(set(current_enemies + new_enemies))\n        if mode == SelectMode.REPLACE:\n            return new_enemies\n        return new_enemies\n\n    def get_all_enemies(self) -> list[core.Enemy]:\n        enemies: list[core.Enemy] = []\n        for i in range(len(self.save_file.enemy_guide)):\n            enemies.append(core.Enemy(i))\n        return enemies\n\n    def get_all_valid_enemies(self) -> list[core.Enemy] | None:\n        valid_ids = core.EnemyDictionary(self.save_file).get_valid_enemies()\n        if valid_ids is None:\n            return None\n\n        return [core.Enemy(id) for id in valid_ids]\n\n    def get_all_invalid_enemies(self) -> list[core.Enemy] | None:\n        invalid_ids = core.EnemyDictionary(self.save_file).get_invalid_enemies(\n            len(self.save_file.enemy_guide)\n        )\n        if invalid_ids is None:\n            return None\n\n        return [core.Enemy(id) for id in invalid_ids]\n\n    def select_id(self) -> list[core.Enemy] | None:\n        enemy_ids = dialog_creator.RangeInput(\n            len(self.save_file.enemy_guide) - 1\n        ).get_input_locale(\"enter_enemy_ids\", {})\n        if enemy_ids is None:\n            return None\n        enemy_ids = [enemy_id - 2 for enemy_id in enemy_ids]\n        return self.get_enemies_by_id(enemy_ids)\n\n    def get_enemies_by_id(self, ids: list[int]) -> list[core.Enemy]:\n        enemies: list[core.Enemy] = []\n        for enemy in self.get_all_enemies():\n            if enemy.id in ids:\n                enemies.append(enemy)\n        return enemies\n\n    def select_name(self) -> list[core.Enemy] | None:\n        usr_name = dialog_creator.StringInput().get_input_locale(\"enter_enemy_name\", {})\n        if usr_name is None:\n            return None\n        enemies = self.get_enemies_by_name(usr_name)\n        if not enemies:\n            color.ColoredText.localize(\"enemy_not_found_name\", name=usr_name)\n            return None\n\n        enemy_names = [enemy.get_name(self.save_file) for enemy in enemies]\n        new_enemy_names: list[str] = []\n        for enemy_name in enemy_names:\n            if enemy_name is None:\n                return None\n\n            new_enemy_names.append(enemy_name)\n\n        enemy_option_ids, _ = dialog_creator.ChoiceInput.from_reduced(\n            new_enemy_names, dialog=\"select_enemies\", single_choice=False\n        ).multiple_choice()\n        if enemy_option_ids is None:\n            return None\n        enemies_selected: list[core.Enemy] = []\n        for enemy_option_id in enemy_option_ids:\n            enemies_selected.append(enemies[enemy_option_id])\n        return enemies_selected\n\n    def get_enemies_by_name(self, name: str) -> list[core.Enemy]:\n        enemies: list[core.Enemy] = []\n        for enemy in self.get_all_enemies():\n            enemy_name = enemy.get_name(self.save_file)\n            if enemy_name is None:\n                continue\n            if name.lower() in enemy_name.lower():\n                enemies.append(enemy)\n        return enemies\n\n    @staticmethod\n    def from_save_file(\n        save_file: core.SaveFile,\n    ) -> tuple[EnemyEditor | None, list[core.Enemy]]:\n        enemy_editor = EnemyEditor(save_file)\n        current_enemies = enemy_editor.select([])\n        if current_enemies is None:\n            return None, []\n        return enemy_editor, current_enemies\n\n    @staticmethod\n    def edit_enemy_guide(\n        save_file: core.SaveFile,\n        current_enemies: list[core.Enemy] | None = None,\n        enemy_editor: EnemyEditor | None = None,\n    ):\n        if enemy_editor is None or current_enemies is None:\n            enemy_editor, current_enemies = EnemyEditor.from_save_file(save_file)\n        if enemy_editor is None or not current_enemies:\n            return\n\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            [\"unlock_enemy_guide\", \"remove_enemy_guide\"],\n            dialog=\"edit_enemy_guide\",\n            single_choice=True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        if choice == 0:\n            enemy_editor.unlock_enemy_guide(current_enemies)\n        elif choice == 1:\n            enemy_editor.remove_enemy_guide(current_enemies)\n"
  },
  {
    "path": "src/bcsfe/cli/edits/event_tickets.py",
    "content": "from __future__ import annotations\nfrom bcsfe import cli, core\nfrom bcsfe.core.game.catbase.gatya import GatyaEventType\nfrom bcsfe.core.server.event_data import split_hhmm, split_yyyymmdd\n\n\nclass EventTickets:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.gatya_item_buy = core.core_data.get_gatya_item_buy(self.save_file)\n        self.gatya_item_names = core.core_data.get_gatya_item_names(self.save_file)\n        self.gatya_option_n = core.GatyaDataOption.read(\n            self.save_file, GatyaEventType.NORMAL\n        )\n        self.gatya_option_r = core.GatyaDataOption.read(\n            self.save_file, GatyaEventType.RARE\n        )\n        self.gatya_option_e = core.GatyaDataOption.read(\n            self.save_file, GatyaEventType.EVENT\n        )\n\n        cli.color.ColoredText.localize(\"downloading_gatya_data\")\n        temp_save_file = core.SaveFile(cc=save_file.cc, gv=save_file.game_version)\n        gatya_event_data = core.ServerHandler(temp_save_file).download_gatya_data()\n\n        if gatya_event_data is None:\n            cli.color.ColoredText.localize(\"download_gatya_data_fail\")\n            self.gatya_event_data = None\n        else:\n            cli.color.ColoredText.localize(\"download_gatya_data_success\")\n            self.gatya_event_data = core.ServerGatyaData.from_data(gatya_event_data)\n\n    @staticmethod\n    def edit(save_file: core.SaveFile):\n        event_tickets = EventTickets(save_file)\n\n        if event_tickets.gatya_event_data is None:\n            return\n\n        event_ticket_items: list[\n            tuple[\n                core.ServerGatyaDataItem, core.ServerGatyaDataSet, core.GatyaItemBuyItem\n            ]\n        ] = []\n\n        if (\n            event_tickets.gatya_option_n is None\n            or event_tickets.gatya_option_r is None\n            or event_tickets.gatya_option_e is None\n        ):\n            return\n\n        for item in event_tickets.gatya_event_data.items:\n            for gset in item.sets:\n                if gset.number == -1:\n                    continue\n\n                gset_opt = None\n\n                if item.get_normal_flag():\n                    gset_opt = event_tickets.gatya_option_n.get(gset.number)\n                elif item.get_rare_flag():\n                    gset_opt = event_tickets.gatya_option_r.get(gset.number)\n                elif item.get_collab_flag():\n                    gset_opt = event_tickets.gatya_option_e.get(gset.number)\n\n                if gset_opt is None:\n                    continue\n\n                gatya_item = event_tickets.gatya_item_buy.get(gset_opt.ticket_item_id)\n                if gatya_item is None:\n                    continue\n\n                category = gatya_item.category\n                if category in [\n                    core.GatyaItemCategory.EVENT_TICKETS.value,\n                    core.GatyaItemCategory.LUCKY_TICKETS_1.value,\n                    core.GatyaItemCategory.LUCKY_TICKETS_2.value,\n                ]:\n                    event_ticket_items.append((item, gset, gatya_item))\n\n        event_names: list[str] = []\n        values: list[int] = []\n\n        for event_item, gset, gatya_item in event_ticket_items:\n            start_y, start_m, start_d = split_yyyymmdd(event_item.filter.start_yyyymmdd)\n            start_h, start_min = split_hhmm(event_item.filter.start_hhmm)\n            end_y, end_m, end_d = split_yyyymmdd(event_item.filter.end_yyyymmdd)\n            end_h, end_min = split_hhmm(event_item.filter.end_hhmm)\n            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}\"\n            event_message = gset.message.replace(\"<br>\", \"\\n\")\n\n            base_msg = f\"{time_str}\"\n            item_name = event_tickets.gatya_item_names.get_name(gatya_item.id)\n            if item_name is not None:\n                base_msg += f\" - {item_name}\"\n\n            if event_message:\n                base_msg += f\" - {event_message}\"\n\n            current_amount = event_tickets.get_ticket(gatya_item.id)\n\n            if current_amount is not None:\n                event_names.append(base_msg)\n                values.append(current_amount)\n\n        values = cli.dialog_creator.MultiEditor.from_reduced(\n            \"event_tickets\",\n            event_names,\n            ints=values,\n            max_values=core.core_data.max_value_manager.get(\"event_tickets\"),\n            group_name_localized=True,\n        ).edit()\n\n        for (event_item, gset, gatya_item), value in zip(event_ticket_items, values):\n            event_tickets.edit_ticket(gatya_item.id, value)\n\n    def get_ticket(self, item_id: int) -> int | None:\n        item = self.gatya_item_buy.get(item_id)\n        if item is None:\n            return\n\n        if item.category == core.GatyaItemCategory.EVENT_TICKETS.value:\n            if item.index < len(self.save_file.event_capsules):\n                return self.save_file.event_capsules[item.index]\n        if item.category == core.GatyaItemCategory.LUCKY_TICKETS_1.value:\n            if item.index < len(self.save_file.lucky_tickets):\n                return self.save_file.lucky_tickets[item.index]\n        if item.category == core.GatyaItemCategory.LUCKY_TICKETS_2.value:\n            if item.index < len(self.save_file.event_capsules_2):\n                return self.save_file.event_capsules_2[item.index]\n\n        return None\n\n    def edit_ticket(self, item_id: int, amount: int):\n        item = self.gatya_item_buy.get(item_id)\n        if item is None:\n            return\n\n        if item.category == core.GatyaItemCategory.EVENT_TICKETS.value:\n            if item.index < len(self.save_file.event_capsules):\n                self.save_file.event_capsules[item.index] = amount\n        if item.category == core.GatyaItemCategory.LUCKY_TICKETS_1.value:\n            if item.index < len(self.save_file.lucky_tickets):\n                self.save_file.lucky_tickets[item.index] = amount\n        if item.category == core.GatyaItemCategory.LUCKY_TICKETS_2.value:\n            if item.index < len(self.save_file.event_capsules_2):\n                self.save_file.event_capsules_2[item.index] = amount\n"
  },
  {
    "path": "src/bcsfe/cli/edits/fixes.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import color\nimport datetime\n\n\nclass Fixes:\n    @staticmethod\n    def fix_gamatoto_crash(save_file: core.SaveFile):\n        save_file.gamatoto.skin = 2\n\n        color.ColoredText.localize(\"fix_gamatoto_crash_success\")\n\n    @staticmethod\n    def fix_ototo_crash(save_file: core.SaveFile):\n        save_file.ototo.cannons = core.game.gamoto.ototo.Cannons.init(\n            save_file.game_version\n        )\n        color.ColoredText.localize(\"fix_ototo_crash_success\")\n\n    @staticmethod\n    def fix_time_errors(save_file: core.SaveFile):\n        save_file.date_3 = datetime.datetime.now()\n        save_file.timestamp = datetime.datetime.now().timestamp()\n        save_file.energy_penalty_timestamp = datetime.datetime.now().timestamp()\n\n        color.ColoredText.localize(\"fix_time_errors_success\")\n\n        # 10 = 62 / hgt1 = ahead by too much\n        # 11 = 63 / hgt0 = behind by too much\n        # 12 = 61 / hgt2 = ahead by too much\n\n        # date_3 - controls gacha errors (hgt2)\n        # can't be ahead of the device time\n\n        # timestamp - controls gacha errors (hgt1, hgt0)\n        # can't be ahead by more than 10 minutes to device time\n        # can't be behind by more than 1.5 days to device time\n\n        # penalty_timestamp - controls energy / gamatoto errors\n        # can't by ahead of device time\n        # can't be ahead by more than 1 day to device time\n        # can't be behind by more than 1 day to device time\n"
  },
  {
    "path": "src/bcsfe/cli/edits/map.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\nfrom typing import Union\n\nChaptersType = Union[\n    \"core.EventChapters\",\n    \"core.GauntletChapters\",\n    \"core.LegendQuestChapters\",\n    \"core.ZeroLegendsChapters\",\n    \"core.Chapters\",\n]\n\n\ndef get_total_maps(chapters: ChaptersType) -> int:\n    if isinstance(chapters, core.EventChapters):\n        return chapters.get_lengths()[1]\n    return len(chapters.chapters)\n\n\ndef unclear_stage(\n    chapters: ChaptersType,\n    map: int,\n    star: int,\n    stage: int,\n    type: int | None = None,\n) -> bool:\n    if isinstance(chapters, core.EventChapters):\n        if type is None:\n            raise ValueError(\"Type must be specified for EventChapters!\")\n        return chapters.unclear_stage(type, map, star, stage)\n    else:\n        return chapters.unclear_stage(map, star, stage)\n\n\ndef clear_stage(\n    chapters: ChaptersType,\n    map: int,\n    star: int,\n    stage: int,\n    clear_amount: int = 1,\n    overwrite_clear_progress: bool = False,\n    type: int | None = None,\n    ensure_cleared_only: bool = False,\n) -> bool:\n    if isinstance(chapters, core.EventChapters):\n        if type is None:\n            raise ValueError(\"Type must be specified for EventChapters!\")\n\n        return chapters.clear_stage(\n            type, map, star, stage, clear_amount, overwrite_clear_progress\n        )\n    else:\n        return chapters.clear_stage(\n            map,\n            star,\n            stage,\n            clear_amount,\n            overwrite_clear_progress,\n            ensure_cleared_only=ensure_cleared_only,\n        )\n\n\ndef unclear_rest(\n    chapters: ChaptersType,\n    stages: list[int],\n    stars: int,\n    id: int,\n    type: int | None = None,\n):\n    if isinstance(chapters, core.EventChapters):\n        if type is None:\n            raise ValueError(\"Type must be specified for EventChapters!\")\n        chapters.unclear_rest(stages, stars, id, type)\n    else:\n        chapters.unclear_rest(stages, stars, id)\n\n\ndef get_total_stars(\n    map_option: core.MapOption,\n    base_index: int,\n    chapters: ChaptersType,\n    id: int,\n    type: int | None = None,\n) -> int:\n\n    max_stars = get_max_stars(chapters, id, type)\n\n    map_option_stars = map_option.get_map(base_index + id)\n    if map_option_stars is not None:\n        return min(max_stars, map_option_stars.crown_count)\n    return max_stars\n\n\ndef get_max_max_stars(\n    map_option: core.MapOption,\n    base_index: int,\n    ids: list[int],\n    chapters: ChaptersType,\n    type: int | None = None,\n) -> int:\n    m = 0\n    for id in ids:\n        val = get_total_stars(map_option, base_index, chapters, id, type)\n        if val > m:\n            m = val\n\n    return m\n\n\ndef get_max_stars(\n    chapters: ChaptersType,\n    id: int,\n    type: int | None = None,\n) -> int:\n\n    if isinstance(chapters, core.EventChapters):\n        if type is None:\n            raise ValueError(\"Type must be specified for EventChapters!\")\n        max_stars = chapters.get_total_stars(type, id)\n    else:\n        max_stars = chapters.get_total_stars(id)\n\n    return max_stars\n\n\ndef get_total_stages(\n    chapters: ChaptersType, id: int, star: int, type: int | None = None\n):\n    if isinstance(chapters, core.EventChapters):\n        if type is None:\n            raise ValueError(\"Type must be specified for EventChapters!\")\n        total_stars = chapters.get_total_stages(type, id, star)\n    else:\n        total_stars = chapters.get_total_stages(id, star)\n\n    return total_stars\n\n\ndef select_maps(\n    save_file: core.SaveFile,\n    chapters: ChaptersType,\n    letter_code: str,\n    base_index: int,\n    no_r_prefix: bool = False,\n) -> list[int] | None:\n    map_names = core.MapNames(\n        save_file, letter_code, no_r_prefix=no_r_prefix, base_index=base_index\n    )\n    names: dict[int, str | None] = {}\n    for id, name in map_names.map_names.items():\n        if id >= get_total_maps(chapters):\n            continue\n        names[id] = name\n\n    return core.EventChapters.select_map_names(names)\n\n\ndef select_maps_stars(\n    save_file: core.SaveFile,\n    map_option: core.MapOption,\n    chapters: ChaptersType,\n    letter_code: str,\n    base_index: int,\n    type: int | None = None,\n    no_r_prefix: bool = False,\n) -> list[tuple[int, int]] | None:\n    map_names = core.MapNames(\n        save_file, letter_code, no_r_prefix=no_r_prefix, base_index=base_index\n    )\n    names: dict[int, str | None] = {}\n    for id, name in map_names.map_names.items():\n        if id >= get_total_maps(chapters):\n            continue\n\n        for star in range(get_total_stars(map_option, base_index, chapters, id, type)):\n            names[id * 10 + star] = core.localize(\n                \"map_name_star\", name=name, star=star + 1\n            )\n\n    ids = core.EventChapters.select_map_names(names)\n    if ids is None:\n        return None\n\n    new_ids: list[tuple[int, int]] = []\n\n    for id in ids:\n        map_id = id // 10\n        star_index = id % 10\n\n        new_ids.append((map_id, star_index))\n\n    return new_ids\n\n\ndef edit_chapters2_clear_count(\n    save_file: core.SaveFile,\n    chapters: ChaptersType,\n    letter_code: str,\n    base_index: int,\n    type: int | None = None,\n    no_r_prefix: bool = False,\n):\n\n    map_names = core.MapNames(\n        save_file, letter_code, no_r_prefix=no_r_prefix, base_index=base_index\n    )\n\n    map_option = core.MapOption.from_save(save_file)\n    if map_option is None:\n        return None\n\n    map_choices = select_maps_stars(\n        save_file, map_option, chapters, letter_code, base_index, type, no_r_prefix\n    )\n    if map_choices is None:\n        return None\n\n    clear_all = edit_all_or_handle_ind(len(map_choices))\n    if clear_all is None:\n        return None\n\n    if clear_all == 0:\n        clear_count = core.EventChapters.ask_clear_amount()\n        if clear_count is None:\n            return None\n\n        for local_map_id, star in map_choices:\n            total_stages = get_total_stages(chapters, local_map_id, star, type)\n            for stage in range(total_stages):\n                clear_stage(chapters, local_map_id, star, stage, clear_count, type=type)\n    else:\n        for local_map_id, star in map_choices:\n            print()\n            core.EventChapters.print_current_chapter(\n                core.localize(\n                    \"map_name_star\",\n                    star=star,\n                    name=map_names.map_names.get(local_map_id),\n                ),\n                local_map_id,\n            )\n            clear_whole = dialog_creator.ChoiceInput.from_reduced(\n                [\"edit_whole_chapter\", \"edit_specific_stages\"], dialog=\"edit_chapter_q\"\n            ).single_choice()\n            if clear_whole is None:\n                return None\n\n            clear_whole -= 1\n\n            if clear_whole == 0:\n                clear_count = core.EventChapters.ask_clear_amount()\n                if clear_count is None:\n                    return None\n\n                for stage in range(\n                    get_total_stages(chapters, local_map_id, star, type)\n                ):\n                    clear_stage(\n                        chapters, local_map_id, star, stage, clear_count, type=type\n                    )\n            else:\n                stage_ids = core.EventChapters.ask_stages(map_names, local_map_id)\n\n                if stage_ids is None:\n                    return None\n\n                all_selected_stages = dialog_creator.ChoiceInput.from_reduced(\n                    [\"each_stage_individually\", \"stage_all_at_once\"],\n                    dialog=\"set_clear_count_stage_q\",\n                ).single_choice()\n                if all_selected_stages is None:\n                    return None\n\n                all_selected_stages -= 1\n\n                stage_names = core.EventChapters.get_stage_names(\n                    map_names, local_map_id\n                )\n                if stage_names is None:\n                    stage_names = []\n                if all_selected_stages == 0:\n                    for stage in stage_ids:\n                        print()\n                        if stage < len(stage_names):\n                            stage_name = stage_names[stage]\n                        else:\n                            stage_name = None\n                        core.EventChapters.print_current_stage(stage_name, stage)\n                        clear_count = core.EventChapters.ask_clear_amount()\n                        if clear_count is None:\n                            return None\n                        clear_stage(\n                            chapters, local_map_id, star, stage, clear_count, type=type\n                        )\n                else:\n                    clear_count = core.EventChapters.ask_clear_amount()\n                    if clear_count is None:\n                        return None\n                    for stage in stage_ids:\n                        clear_stage(\n                            chapters, local_map_id, star, stage, clear_count, type=type\n                        )\n\n\ndef clear_all_or_handle_ind(map_choices_len: int) -> int | None:\n    if map_choices_len <= 1:\n        clear_all = 1\n    else:\n        clear_all = dialog_creator.ChoiceInput.from_reduced(\n            [\"clear_all\", \"handle_individually\"], dialog=\"clear_chapters_q\"\n        ).single_choice()\n        if clear_all is None:\n            return None\n\n        clear_all -= 1\n\n    return clear_all\n\n\ndef unclear_all_or_handle_ind(map_choices_len: int) -> int | None:\n    if map_choices_len <= 1:\n        clear_all = 1\n    else:\n        clear_all = dialog_creator.ChoiceInput.from_reduced(\n            [\"unclear_all\", \"handle_individually\"], dialog=\"unclear_chapters_q\"\n        ).single_choice()\n        if clear_all is None:\n            return None\n\n        clear_all -= 1\n\n    return clear_all\n\n\ndef edit_all_or_handle_ind(map_choices_len: int) -> int | None:\n    if map_choices_len <= 1:\n        clear_all = 1\n    else:\n        clear_all = dialog_creator.ChoiceInput.from_reduced(\n            [\"edit_map_all\", \"handle_individually\"], dialog=\"edit_chapters_q_all\"\n        ).single_choice()\n        if clear_all is None:\n            return None\n\n        clear_all -= 1\n\n    return clear_all\n\n\ndef edit_chapters2_progress(\n    save_file: core.SaveFile,\n    chapters: ChaptersType,\n    letter_code: str,\n    base_index: int,\n    type: int | None = None,\n    no_r_prefix: bool = False,\n    allow_unclear: bool = False,\n):\n    map_names = core.MapNames(\n        save_file, letter_code, no_r_prefix=no_r_prefix, base_index=base_index\n    )\n\n    map_choices = select_maps(save_file, chapters, letter_code, base_index, no_r_prefix)\n    if map_choices is None:\n        return None\n\n    clear_all = clear_all_or_handle_ind(len(map_choices))\n    if clear_all is None:\n        return None\n\n    map_option = core.MapOption.from_save(save_file)\n    if map_option is None:\n        return None\n\n    if clear_all == 0:\n        max_stars = get_max_max_stars(\n            map_option, base_index, map_choices, chapters, type\n        )\n        if allow_unclear:\n            stars = core.EventChapters.ask_stars_unclear(max_stars, \"max_stars\")\n        else:\n            stars = core.EventChapters.ask_stars(max_stars, \"max_stars\")\n        if stars is None:\n            return None\n        for local_map_id in map_choices:\n            unclear_rest(\n                chapters,\n                [0],\n                max(0, stars - 1),\n                local_map_id,\n                type,\n            )\n            for star in range(stars):\n                total_stages = get_total_stages(chapters, local_map_id, star, type)\n                for stage in range(total_stages):\n                    clear_stage(\n                        chapters,\n                        local_map_id,\n                        star,\n                        stage,\n                        type=type,\n                        ensure_cleared_only=True,\n                    )\n\n        return map_choices\n\n    for local_map_id in map_choices:\n        name = map_names.map_names.get(local_map_id)\n        core.EventChapters.print_current_chapter(name, local_map_id)\n        clear_whole = dialog_creator.ChoiceInput.from_reduced(\n            [\"clear_whole_chapter\", \"clear_to_specific_stage\"], dialog=\"clear_whole_q\"\n        ).single_choice()\n        if clear_whole is None:\n            return None\n\n        clear_whole -= 1\n\n        if clear_whole == 0:\n            max_stars = get_total_stars(\n                map_option, base_index, chapters, local_map_id, type\n            )\n            if allow_unclear:\n                stars = core.EventChapters.ask_stars_unclear(max_stars)\n            else:\n                stars = core.EventChapters.ask_stars(max_stars)\n            if stars is None:\n                return None\n\n            unclear_rest(\n                chapters,\n                [0],\n                max(stars - 1, 0),\n                local_map_id,\n                type,\n            )\n\n            for star in range(stars):\n                total_stages = get_total_stages(chapters, local_map_id, star, type)\n                for stage in range(total_stages):\n                    clear_stage(\n                        chapters,\n                        local_map_id,\n                        star,\n                        stage,\n                        type=type,\n                        ensure_cleared_only=True,\n                    )\n\n        else:\n            stage_names = map_names.stage_names.get(local_map_id)\n            stage_names = [\n                stage_name\n                for stage_name in stage_names or []\n                if stage_name and stage_name != \"＠\"\n            ]\n            stage_id = core.EventChapters.ask_stages_stage_names_one(stage_names)\n            if stage_id is None:\n                return None\n\n            max_stars = get_total_stars(\n                map_option, base_index, chapters, local_map_id, type\n            )\n\n            if allow_unclear:\n                stars = core.EventChapters.ask_stars_unclear(max_stars)\n            else:\n                stars = core.EventChapters.ask_stars(max_stars)\n            if stars is None:\n                return None\n\n            unclear_rest(\n                chapters, list(range(stage_id)), max(stars - 1, 0), local_map_id, type\n            )\n\n            for star in range(stars - 1):\n                total_stages = get_total_stages(chapters, local_map_id, star, type)\n                for stage in range(total_stages):\n                    clear_stage(\n                        chapters,\n                        local_map_id,\n                        star,\n                        stage,\n                        type=type,\n                        ensure_cleared_only=True,\n                    )\n\n            for stage in range(stage_id + 1):\n                clear_stage(\n                    chapters,\n                    local_map_id,\n                    stars - 1,\n                    stage,\n                    type=type,\n                    ensure_cleared_only=True,\n                )\n\n\ndef edit_chapters(\n    save_file: core.SaveFile,\n    chapters: ChaptersType,\n    letter_code: str,\n    base_index: int,\n    type: int | None = None,\n    no_r_prefix: bool = False,\n) -> dict[int, bool] | None:\n    while True:\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            [\n                \"edit_progress_clear\",\n                \"edit_progress_unclear\",\n                \"edit_clear_counts\",\n                \"finish\",\n            ],\n            dialog=\"edit_chapters_q\",\n        ).single_choice()\n        if choice is None:\n            return None\n        choice -= 1\n\n        if choice == 0:\n            edit_chapters2_progress(\n                save_file, chapters, letter_code, base_index, type, no_r_prefix\n            )\n        elif choice == 1:\n            edit_chapters2_progress(\n                save_file,\n                chapters,\n                letter_code,\n                base_index,\n                type,\n                no_r_prefix,\n                allow_unclear=True,\n            )\n        elif choice == 2:\n            edit_chapters2_clear_count(\n                save_file, chapters, letter_code, base_index, type, no_r_prefix\n            )\n        else:\n            break\n        color.ColoredText.localize(\"map_chapters_edited\")\n    color.ColoredText.localize(\"map_chapters_edited\")\n\n    return None\n"
  },
  {
    "path": "src/bcsfe/cli/edits/max_all.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom bcsfe import core\n\n\ndef max_catfood(save_file: core.SaveFile):\n    orig = save_file.catfood\n    save_file.catfood = core.core_data.max_value_manager.get(core.MaxValueType.CATFOOD)\n    core.BackupMetaData(save_file).add_managed_item(\n        core.ManagedItem.from_change(\n            save_file.catfood - orig, core.ManagedItemType.CATFOOD\n        )\n    )\n\n\ndef max_rare_tickets(save_file: core.SaveFile):\n    orig = save_file.rare_tickets\n    save_file.rare_tickets = core.core_data.max_value_manager.get(\n        core.MaxValueType.RARE_TICKETS\n    )\n    core.BackupMetaData(save_file).add_managed_item(\n        core.ManagedItem.from_change(\n            save_file.rare_tickets - orig, core.ManagedItemType.RARE_TICKET\n        )\n    )\n\n\ndef max_plat_tickets(save_file: core.SaveFile):\n    orig = save_file.platinum_tickets\n    save_file.platinum_tickets = core.core_data.max_value_manager.get(\n        core.MaxValueType.PLATINUM_TICKETS\n    )\n    core.BackupMetaData(save_file).add_managed_item(\n        core.ManagedItem.from_change(\n            save_file.platinum_tickets - orig, core.ManagedItemType.PLATINUM_TICKET\n        )\n    )\n\n\ndef max_plat_shards(save_file: core.SaveFile):\n    save_file.platinum_shards = 10 * core.core_data.max_value_manager.get(\n        core.MaxValueType.PLATINUM_TICKETS\n    )\n\n\ndef max_legend_tickets(save_file: core.SaveFile):\n    orig = save_file.legend_tickets\n    save_file.legend_tickets = core.core_data.max_value_manager.get(\n        core.MaxValueType.LEGEND_TICKETS\n    )\n    core.BackupMetaData(save_file).add_managed_item(\n        core.ManagedItem.from_change(\n            save_file.legend_tickets - orig, core.ManagedItemType.LEGEND_TICKET\n        )\n    )\n\n\ndef max_xp(save_file: core.SaveFile):\n    save_file.xp = core.core_data.max_value_manager.get(core.MaxValueType.XP)\n\n\ndef max_np(save_file: core.SaveFile):\n    save_file.np = core.core_data.max_value_manager.get(core.MaxValueType.NP)\n\n\ndef max_100_million_ticket(save_file: core.SaveFile):\n    save_file.hundred_million_ticket = core.core_data.max_value_manager.get(\n        core.MaxValueType.HUNDRED_MILLION_TICKETS\n    )\n\n\ndef max_leadership(save_file: core.SaveFile):\n    save_file.leadership = core.core_data.max_value_manager.get(\n        core.MaxValueType.LEADERSHIP\n    )\n\n\ndef max_battle_items(save_file: core.SaveFile):\n    for item in save_file.battle_items.items:\n        item.amount = core.core_data.max_value_manager.get(\n            core.MaxValueType.BATTLE_ITEMS\n        )\n\n\ndef max_catseyes(save_file: core.SaveFile):\n    for id in range(len(save_file.catseyes)):\n        save_file.catseyes[id] = core.core_data.max_value_manager.get(\n            core.MaxValueType.CATSEYES\n        )\n\n\ndef max_treasure_chests(save_file: core.SaveFile):\n    for id in range(len(save_file.treasure_chests)):\n        save_file.treasure_chests[id] = core.core_data.max_value_manager.get(\n            core.MaxValueType.TREASURE_CHESTS\n        )\n\n\ndef max_catamins(save_file: core.SaveFile):\n    for id in range(len(save_file.catseyes)):\n        save_file.catamins[id] = core.core_data.max_value_manager.get(\n            core.MaxValueType.CATAMINS\n        )\n\n\ndef max_labyrinth_medals(save_file: core.SaveFile):\n    for id in range(len(save_file.labyrinth_medals)):\n        save_file.labyrinth_medals[id] = core.core_data.max_value_manager.get(\n            core.MaxValueType.LABYRINTH_MEDALS\n        )\n\n\n# def max_catfruit(save_file: core.SaveFile):\n#     for id in range(len(save_file.catfruit)):\n#         save_file.catfruit[id] = core.core_data.max_value_manager.get_new(\n#             core.MaxValueType.CATFRUIT\n#         )\n\n\ndef max_normal_tickets(save_file: core.SaveFile):\n    save_file.normal_tickets = core.core_data.max_value_manager.get(\n        core.MaxValueType.NORMAL_TICKETS\n    )\n\n\ndef max_all(save_file: core.SaveFile):\n    maxes = core.core_data.max_value_manager\n    features: dict[str, Callable[[core.SaveFile], None]] = {\n        \"catfood\": max_catfood,\n        \"xp\": max_xp,\n        \"normal_tickets\": max_normal_tickets,\n        \"rare_tickets\": max_rare_tickets,\n        \"platinum_tickets\": max_plat_tickets,\n        \"legend_tickets\": max_legend_tickets,\n        \"platinum_shards\": max_plat_shards,\n        \"np\": max_np,\n        \"leadership\": max_leadership,\n        \"battle_items\": max_battle_items,\n        \"catseyes\": max_catseyes,\n        \"catamins\": max_catamins,\n        \"labyrinth_medals\": max_labyrinth_medals,\n        \"100_million_ticket\": max_100_million_ticket,\n        \"treasure_chests\": max_treasure_chests,\n    }\n    # TODO: finish\n"
  },
  {
    "path": "src/bcsfe/cli/edits/rare_ticket_trade.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass RareTicketTrade:\n    @staticmethod\n    def rare_ticket_trade(save_file: core.SaveFile):\n        current_amount = save_file.rare_tickets\n        max_amount = max(\n            core.core_data.max_value_manager.get(\"rare_tickets\")\n            - current_amount,\n            0,\n        )\n        if max_amount == 0:\n            color.ColoredText.localize(\"rare_ticket_trade_maxed\")\n            return\n        to_add = dialog_creator.IntInput(max_amount, 0).get_input_locale_while(\n            \"rare_ticket_trade_enter\",\n            {\"max\": max_amount, \"current\": current_amount},\n        )\n        if to_add is None:\n            return\n\n        space = False\n        for storage_item in save_file.cats.storage_items:\n            if storage_item.item_type == 0 or (\n                storage_item.item_id == 1 and storage_item.item_type == 2\n            ):\n                storage_item.item_id = 1\n                storage_item.item_type = 2\n                space = True\n                break\n\n        if not space:\n            color.ColoredText.localize(\"rare_ticket_trade_storage_full\")\n            return\n\n        amount = to_add * 5\n        save_file.gatya.trade_progress = amount\n\n        color.ColoredText.localize(\n            \"rare_ticket_successfully_traded\", rare_ticket_count=to_add\n        )\n"
  },
  {
    "path": "src/bcsfe/cli/edits/storage.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\nfrom bcsfe.cli.edits import cat_editor\n\n\ndef display_storage(save_file: core.SaveFile, storage: list[core.StorageItem]):\n    color.ColoredText.localize(\"current_storage_items\")\n    index = 0\n    for item in storage:\n        if item.item_type == 0:\n            continue\n\n        index += 1\n        color.ColoredText(f\"{index}. \", end=\"\")\n        display_item(item, save_file)\n\n    if index == 0:\n        color.ColoredText.localize(\"storage_is_empty\")\n\n    available_slots = len(storage) - index\n\n    color.ColoredText.localize(\"available_storage\", slots=available_slots)\n\n\ndef display_item(item: core.StorageItem, save_file: core.SaveFile):\n    color.ColoredText(get_item_str(item, save_file))\n\n\ndef get_item_str(item: core.StorageItem, save_file: core.SaveFile) -> str:\n    if item.item_type == 1:\n        cat_id = item.item_id\n        names = core.Cat.get_names(cat_id, save_file)\n\n        if not names:\n            names = [str(cat_id)]\n\n        return core.localize(\"cat\", name=names[0], id=cat_id)\n    elif item.item_type == 2:\n        skill_id = item.item_id\n\n        skill_names = (\n            core.core_data.get_gatya_item_buy(save_file).get_names_by_category(\n                core.GatyaItemCategory.SPECIAL_SKILLS\n            )\n            or []\n        )\n\n        if skill_id >= len(skill_names) or skill_id < 0:\n            name = str(skill_id)\n        else:\n            name = skill_names[skill_id][1]\n\n        return core.localize(\"special_skill\", name=name, id=skill_id)\n    elif item.item_type == 3:\n        item_id = item.item_id\n\n        name = core.core_data.get_gatya_item_names(save_file).get_name(item_id)\n        if name is None:\n            name = str(item_id)\n\n        return core.localize(\"item\", name=name, id=item_id)\n    else:\n        return core.localize(\n            \"unrecognised_storage_item\", item_type=item.item_type, id=item.item_id\n        )\n\n\ndef clear_storage(storage: list[core.StorageItem]):\n    for item in storage:\n        item.item_id = 0\n        item.item_type = 0\n\n\ndef add_item(storage: list[core.StorageItem], item: core.StorageItem) -> bool:\n    for citem in storage:\n        if citem.item_type == 0:\n            citem.item_type = item.item_type\n            citem.item_id = item.item_id\n            return True\n    return False\n\n\ndef get_storage_space(storage: list[core.StorageItem]) -> int:\n    space = 0\n\n    for item in storage:\n        if item.item_type == 0:\n            space += 1\n    return space\n\n\ndef edit_storage(save_file: core.SaveFile):\n    display_storage(save_file, save_file.cats.storage_items)\n    exit = False\n    while not exit:\n        exit = edit_loop(save_file)\n\n    color.ColoredText.localize(\"storage_success\")\n\n\ndef edit_loop(save_file: core.SaveFile) -> bool:\n    storage = save_file.cats.storage_items\n\n    options = [\n        \"display_storage\",\n        \"clear_storage\",\n        \"add_cats\",\n        \"add_special_skills\",\n        \"remove_items\",\n        \"finish\",\n    ]\n\n    choice = dialog_creator.ChoiceInput.from_reduced(\n        options, dialog=\"select_option\"\n    ).single_choice()\n    if choice is None:\n        return False\n\n    choice -= 1\n\n    if choice == 0:\n        display_storage(save_file, storage)\n    if choice == 1:\n        clear_storage(storage)\n    elif choice == 2:\n        editor, cats = cat_editor.CatEditor.from_save_file(save_file)\n        if editor is None:\n            return False\n\n        space = get_storage_space(storage)\n        if len(cats) > len(storage):\n            color.ColoredText.localize(\n                \"too_many_cats_selected\", max=len(storage), current=len(cats)\n            )\n            return False\n\n        needs = len(cats) - space\n        if needs > 0:\n            color.ColoredText.localize(\"need_x_more_space\", needs=needs)\n            return False\n\n        color.ColoredText.localize(\"added_cats\")\n        for cat in cats:\n            item = core.StorageItem.from_cat(cat.id)\n            add_item(storage, item)\n            display_item(item, save_file)\n    elif choice == 3:\n\n        skill_names: list[str] = list(\n            map(\n                lambda sk: sk[1] or str(sk[0].id),\n                core.core_data.get_gatya_item_buy(save_file).get_names_by_category(\n                    core.GatyaItemCategory.SPECIAL_SKILLS\n                )\n                or [],\n            )\n        )\n\n        options, _ = dialog_creator.ChoiceInput.from_reduced(\n            skill_names, localize_options=False, dialog=\"select_special_skills\"\n        ).multiple_choice(False)\n\n        if options is None:\n            return False\n\n        space = get_storage_space(storage)\n        if len(options) > len(storage):\n            color.ColoredText.localize(\n                \"too_many_skills_selected\", max=len(storage), current=len(options)\n            )\n            return False\n\n        needs = len(options) - space\n        if needs > 0:\n            color.ColoredText.localize(\"need_x_more_space\", needs=needs)\n            return False\n\n        color.ColoredText.localize(\"added_special_skills\")\n        for choice in options:\n            item = core.StorageItem.from_special_skill(choice)\n            add_item(storage, item)\n            display_item(item, save_file)\n\n    elif choice == 4:\n        options2: list[str] = []\n        for item in storage:\n            if item.item_type == 0:\n                continue\n            options2.append(get_item_str(item, save_file))\n\n        choices, _ = dialog_creator.ChoiceInput.from_reduced(\n            options2, localize_options=False\n        ).multiple_choice(False)\n        if choices is None:\n            return False\n\n        color.ColoredText.localize(\"removed_items\")\n        index = 0\n        for item in storage:\n            if item.item_type == 0:\n                continue\n\n            if index in choices:\n                display_item(item, save_file)\n                item.item_type = 0\n                item.item_id = 0\n\n            index += 1\n\n    elif choice == 5:\n        return True\n\n    return False\n"
  },
  {
    "path": "src/bcsfe/cli/feature_handler.py",
    "content": "from __future__ import annotations\nfrom typing import Any, Callable\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color, edits, save_management, main\n\n\nclass FeatureHandler:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n\n    def get_features(self) -> dict[str, Any]:\n        cat_features = {\"cats\": edits.cat_editor.CatEditor.edit_cats}\n        if core.core_data.config.get_bool(core.ConfigKey.SEPARATE_CAT_EDIT_OPTIONS):\n            cat_features = {\n                \"unlock_remove_cats\": edits.cat_editor.CatEditor.unlock_remove_cats_run,\n                \"upgrade_cats\": edits.cat_editor.CatEditor.upgrade_cats_run,\n                \"true_form_remove_form_cats\": edits.cat_editor.CatEditor.true_form_remove_form_cats_run,\n                \"force_true_form_cats\": edits.cat_editor.CatEditor.force_true_form_cats_run,\n                \"fourth_form_remove_form_cats\": edits.cat_editor.CatEditor.fourth_form_remove_form_cats_run,\n                \"force_fourth_form_cats\": edits.cat_editor.CatEditor.force_fourth_form_cats_run,\n                \"upgrade_talents_remove_talents_cats\": edits.cat_editor.CatEditor.upgrade_talents_remove_talents_cats_run,\n                \"unlock_remove_cat_guide\": edits.cat_editor.CatEditor.unlock_cat_guide_remove_guide_run,\n            }\n\n        cat_features[\"special_skills\"] = (\n            edits.basic_items.BasicItems.edit_special_skills\n        )\n\n        cat_features[\"cat_storage\"] = edits.storage.edit_storage\n\n        features: dict[str, Any] = {\n            \"save_management\": {\n                \"save_save\": save_management.SaveManagement.save_save,\n                \"save_upload\": save_management.SaveManagement.save_upload,\n                \"save_save_file\": save_management.SaveManagement.save_save_dialog,\n                core.localize(\n                    \"save_save_documents\", path=core.SaveFile.get_save_path()\n                ): save_management.SaveManagement.save_save_data_dir,\n                \"waydroid_push\": save_management.SaveManagement.waydroid_push,\n                \"waydroid_push_rerun\": save_management.SaveManagement.waydroid_push_rerun,\n                \"adb_push\": save_management.SaveManagement.adb_push,\n                \"adb_push_rerun\": save_management.SaveManagement.adb_push_rerun,\n                \"root_push\": save_management.SaveManagement.root_push,\n                \"root_push_rerun\": save_management.SaveManagement.root_push_rerun,\n                \"export_save\": save_management.SaveManagement.export_save,\n                \"load_save\": save_management.SaveManagement.load_save,\n                # \"init_save\": save_management.SaveManagement.init_save,\n                \"convert_region\": save_management.SaveManagement.convert_save_cc,\n                \"convert_version\": save_management.SaveManagement.convert_save_gv,\n            },\n            \"items\": {\n                \"catfood\": edits.basic_items.BasicItems.edit_catfood,\n                \"xp\": edits.basic_items.BasicItems.edit_xp,\n                \"normal_tickets\": edits.basic_items.BasicItems.edit_normal_tickets,\n                \"rare_tickets\": edits.basic_items.BasicItems.edit_rare_tickets,\n                \"rare_ticket_trade_feature_name\": edits.rare_ticket_trade.RareTicketTrade.rare_ticket_trade,\n                \"platinum_tickets\": edits.basic_items.BasicItems.edit_platinum_tickets,\n                \"legend_tickets\": edits.basic_items.BasicItems.edit_legend_tickets,\n                \"platinum_shards\": edits.basic_items.BasicItems.edit_platinum_shards,\n                \"np\": edits.basic_items.BasicItems.edit_np,\n                \"leadership\": edits.basic_items.BasicItems.edit_leadership,\n                \"battle_items\": edits.basic_items.BasicItems.edit_battle_items,\n                \"battle_items_endless\": edits.basic_items.BasicItems.edit_battle_items_endless,\n                \"catseyes\": edits.basic_items.BasicItems.edit_catseyes,\n                \"catfruit\": edits.basic_items.BasicItems.edit_catfruit,\n                \"talent_orbs\": core.game.catbase.talent_orbs.SaveOrbs.edit_talent_orbs,\n                \"catamins\": edits.basic_items.BasicItems.edit_catamins,\n                \"scheme_items\": edits.basic_items.BasicItems.edit_scheme_items,\n                \"labyrinth_medals\": edits.basic_items.BasicItems.edit_labyrinth_medals,\n                \"100_million_tickets\": edits.basic_items.BasicItems.edit_100_million_ticket,\n                \"event_tickets\": edits.event_tickets.EventTickets.edit,\n                \"treasure_chests\": edits.basic_items.BasicItems.edit_treasure_chests,\n                \"reset_golden_cat_cpus\": edits.basic_items.BasicItems.reset_golden_cat_cpus,\n            },\n            \"cats_special_skills\": cat_features,\n            \"levels\": {\n                \"clear_tutorial\": edits.clear_tutorial.clear_tutorial,\n                \"clear_story\": core.game.map.story.StoryChapters.clear_story,\n                \"challenge_score\": core.game.map.challenge.edit_challenge_score,\n                \"dojo_score\": core.game.map.dojo.edit_dojo_score,\n                \"add_enigma_stages\": core.game.map.enigma.edit_enigma,\n                \"clear_enigma_stages\": core.game.map.gauntlets.GauntletChapters.edit_enigma_stages,\n                \"unlock_aku_realm\": edits.aku_realm.unlock_aku_realm,\n                \"story_treasures\": core.game.map.story.StoryChapters.edit_treasures,\n                \"outbreaks\": core.game.map.outbreaks.Outbreaks.edit_outbreaks,\n                \"aku_chapters\": core.game.map.aku.AkuChapters.edit_aku_chapters,\n                \"itf_timed_scores\": core.game.map.story.StoryChapters.edit_itf_timed_scores,\n                \"filibuster_reclearing\": edits.basic_items.BasicItems.allow_filibuster_stage_reclearing,\n                \"sol\": core.game.map.event.EventChapters.edit_sol_chapters,\n                \"event\": core.game.map.event.EventChapters.edit_event_chapters,\n                \"collab\": core.game.map.event.EventChapters.edit_collab_chapters,\n                \"gauntlets\": core.game.map.gauntlets.GauntletChapters.edit_gauntlets,\n                \"collab_gauntlets\": core.game.map.gauntlets.GauntletChapters.edit_collab_gauntlets,\n                \"uncanny\": core.game.map.uncanny.UncannyChapters.edit_uncanny,\n                \"catamin_stages\": core.game.map.uncanny.UncannyChapters.edit_catamin_stages,\n                \"behemoth_culling\": core.game.map.gauntlets.GauntletChapters.edit_behemoth_culling,\n                \"legend_quest\": core.game.map.legend_quest.LegendQuestChapters.edit_legend_quest,\n                \"towers\": core.game.map.tower.TowerChapters.edit_towers,\n                \"zero_legends\": core.game.map.zero_legends.ZeroLegendsChapters.edit_zero_legends,\n                \"dojo_catclaw_championships\": core.game.map.zero_legends.ZeroLegendsChapters.edit_catclaw_championships,\n            },\n            \"gamototo\": {\n                \"engineers\": edits.basic_items.BasicItems.edit_engineers,\n                \"base_materials\": edits.basic_items.BasicItems.edit_base_materials,\n                \"gamatoto_xp_level\": core.game.gamoto.gamatoto.edit_xp,\n                \"gamatoto_helpers\": core.game.gamoto.gamatoto.edit_helpers,\n                \"ototo_cat_cannon\": core.game.gamoto.ototo.edit_cannon,\n                \"cat_shrine\": core.game.gamoto.cat_shrine.CatShrine.edit_catshrine,\n            },\n            \"account\": {\n                \"unban_account\": save_management.SaveManagement.unban_account,\n                \"upload_items\": save_management.SaveManagement.upload_items,\n                \"inquiry_code\": edits.basic_items.BasicItems.edit_inquiry_code,\n                \"password_refresh_token\": edits.basic_items.BasicItems.edit_password_refresh_token,\n            },\n            \"gatya\": {\n                \"rare_gatya_seed\": edits.basic_items.BasicItems.edit_rare_gatya_seed,\n                \"normal_gatya_seed\": edits.basic_items.BasicItems.edit_normal_gatya_seed,\n                \"event_gatya_seed\": edits.basic_items.BasicItems.edit_event_gatya_seed,\n            },\n            \"fixes\": {\n                \"fix_gamatoto_crash\": edits.fixes.Fixes.fix_gamatoto_crash,\n                \"fix_ototo_crash\": edits.fixes.Fixes.fix_ototo_crash,\n                \"fix_time_errors\": edits.fixes.Fixes.fix_time_errors,\n                \"unlock_equip_menu\": edits.basic_items.BasicItems.unlock_equip_menu,\n                \"fix_officer_pass_crash\": core.OfficerPass.fix_crash,\n            },\n            \"other\": {\n                \"unlocked_slots\": edits.basic_items.BasicItems.edit_unlocked_slots,\n                \"reset_gambling_events\": core.GamblingEvent.reset_events,\n                \"restart_pack\": edits.basic_items.BasicItems.set_restart_pack,\n                \"special_skills\": edits.basic_items.BasicItems.edit_special_skills,\n                \"playtime\": core.game.catbase.playtime.edit,\n                \"enemy_guide\": edits.enemy_editor.EnemyEditor.edit_enemy_guide,\n                \"user_rank_rewards\": core.game.catbase.user_rank_rewards.edit_user_rank_rewards,\n                \"unlock_equip_menu\": edits.basic_items.BasicItems.unlock_equip_menu,\n                \"gold_pass\": core.game.catbase.nyanko_club.NyankoClub.edit_gold_pass,\n                \"medals\": core.game.catbase.medals.Medals.edit_medals,\n                \"missions\": core.game.catbase.mission.Missions.edit_missions,\n            },\n            \"config\": core.core_data.config.edit_config,\n            \"update_external\": core.update_external_content,\n            \"exit\": main.Main.exit_editor,\n        }\n        return features\n\n    def get_feature(self, feature_path: list[str]):\n        feature_dict = self.get_features()\n        feature = feature_dict\n        for path in feature_path:\n            feature = feature[path]\n\n        return feature\n\n    def search_features(\n        self,\n        name: str,\n        current_path: list[str],\n        features: dict[str, Any] | None = None,\n        found_features: dict[tuple[str, ...], int] | None = None,\n    ) -> dict[tuple[str, ...], int]:\n        name = name.lower()\n        if features is None:\n            features = self.get_features()\n        if found_features is None:\n            found_features = {}\n\n        for feature_name_key, feature in features.items():\n            feature_name = core.core_data.local_manager.get_key(feature_name_key)\n            path = current_path.copy()\n            path.append(feature_name_key)\n            if isinstance(feature, dict):\n                found_features.update(\n                    self.search_features(\n                        name,\n                        path,\n                        feature,  # type: ignore\n                        found_features,\n                    )\n                )\n            for alias in core.LocalManager.get_all_aliases(feature_name):\n                if not name:\n                    found_features[*path] = 100\n                    break\n                alias = alias.lower()\n\n                name = name.replace(\" \", \"\")\n                alias = alias.replace(\" \", \"\")\n                if alias in name or name in alias:\n                    found_features[*path] = 100\n                break\n\n        return found_features\n\n    def display_features(self, features: list[list[str]]):\n        feature_names: list[str] = []\n        for feature_name in features:\n            feature_names.append(feature_name[-1])\n        print()\n        dialog_creator.ListOutput(feature_names, [], \"features\", {}).display_locale(\n            remove_alias=True\n        )\n\n    def select_features(\n        self, features: list[list[str]], current_path: list[str]\n    ) -> list[list[str]]:\n        if features != list(self.get_features().keys()):\n            features.insert(0, [\"go_back\"])\n        self.display_features(features)\n        print()\n        usr_input = color.ColoredInput().localize(\"select_features\").strip()\n        selected_features: list[list[str]] = []\n        if usr_input.isdigit():\n            usr_input = int(usr_input)\n            if usr_input > len(features):\n                color.ColoredText.localize(\"invalid_input\")\n            elif usr_input < 1:\n                color.ColoredText.localize(\"invalid_input\")\n            else:\n                feature_name_top = features[usr_input - 1]\n                if feature_name_top == [\"go_back\"]:\n                    return [[k] for k in self.get_features().keys()]\n                feature = self.get_feature(feature_name_top)\n                if isinstance(feature, dict):\n                    for feature_name in feature.keys():  # type: ignore\n                        feature_path: list[str] = current_path.copy()\n                        feature_path.extend(feature_name_top + [feature_name])\n                        selected_features.append(feature_path)\n\n                else:\n                    feature_path = current_path.copy()\n                    feature_path.extend(feature_name_top)\n                    selected_features.append(feature_path)\n\n        else:\n            feats = self.search_features(usr_input, [])\n            if not feats:\n                color.ColoredText.localize(\"no_feature_with_name\", name=usr_input)\n            kv_map = list(feats.items())\n            kv_map.sort(key=lambda v: v[1], reverse=True)\n            selected_features = [list(v[0]) for v in kv_map]\n\n        return selected_features\n\n    def select_features_run(self):\n        features_dict = self.get_features()\n        features: list[list[str]] = [[k] for k in features_dict.keys()]\n        self.save_file.to_file_thread(self.save_file.get_temp_path())\n        edits.clear_tutorial.clear_tutorial(self.save_file, False)\n        self.save_file.show_ban_message = False\n\n        while True:\n            features = self.select_features(features, [])\n\n            new_features: list[list[str]] = []\n            found_strs: list[str] = []\n            for feature_ in features:\n                if feature_[-1] in found_strs:\n                    continue\n                found_strs.append(feature_[-1])\n                new_features.append(feature_)\n\n            features = new_features\n            feature = None\n            if len(features) == 1:\n                feature = features[0]\n            if len(features) == 2 and features[0] == [\"go_back\"]:\n                feature = features[1]\n\n            if not feature:\n                continue\n\n            feature = self.get_feature(feature)\n\n            if isinstance(feature, Callable):\n                self.do_save_actions()\n\n                feature(self.save_file)\n\n                self.save_file.to_file_thread(self.save_file.get_temp_path())\n\n                features_dict = self.get_features()\n                features = [[k] for k in features_dict.keys()]\n\n                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\n\n    def do_save_actions(self):\n        if core.core_data.config.get_bool(core.ConfigKey.CLEAR_TUTORIAL_ON_LOAD):\n            edits.clear_tutorial.clear_tutorial(self.save_file, False)\n        if core.core_data.config.get_bool(core.ConfigKey.REMOVE_BAN_MESSAGE_ON_LOAD):\n            self.save_file.show_ban_message = False\n"
  },
  {
    "path": "src/bcsfe/cli/file_dialog.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass FileDialog:\n    def load_tk(self):\n        try:\n            import tkinter as tk\n            from tkinter import filedialog\n\n            self.tk = tk\n            self.filedialog = filedialog\n        except ImportError:\n            self.tk = None\n            self.filedialog = None\n\n    def __init__(self):\n        self.load_tk()\n        if self.tk is not None:\n            try:\n                self.root = self.tk.Tk()\n            except self.tk.TclError:\n                self.tk = None\n                self.filedialog = None\n                return\n\n            self.root.withdraw()\n            self.root.wm_attributes(\"-topmost\", 1)  # type: ignore\n\n    def select_files_in_dir(\n        self, path: core.Path, ignore_json: bool\n    ) -> str | None:\n        \"\"\"Print current files in directory.\n\n        Args:\n            path (core.Path): Path to directory.\n        \"\"\"\n        color.ColoredText.localize(\"current_files_dir\", dir=path)\n        path.generate_dirs()\n        files = path.get_files()\n        if not files:\n            color.ColoredText.localize(\"no_files_dir\")\n\n        files.sort(key=lambda file: file.basename())\n\n        # remove files with .json extension\n        if ignore_json:\n            files = [file for file in files if file.get_extension() != \"json\"]\n\n        files_str_ls = [file.basename() for file in files]\n        options = files_str_ls + [core.localize(\"other_dir\"), core.localize(\"another_path\")]\n\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options,\n            dialog=\"select_files_dir\",\n            single_choice=True,\n            localize_options=False,\n        ).single_choice()\n        if choice is None:\n            return\n\n        choice -= 1\n        if choice == len(files):\n            path_input = color.ColoredInput().localize(\"enter_path_dir\")\n            path_obj = core.Path(path_input)\n            if path_obj.is_relative():\n                path_obj = path.add(path_obj)\n            if not path_obj.exists():\n                color.ColoredText.localize(\"path_not_exists\", path=path_obj)\n                return self.select_files_in_dir(path, ignore_json)\n            return self.select_files_in_dir(path_obj, ignore_json)\n        if choice == len(files) + 1:\n            path_input = color.ColoredInput().localize(\"enter_path\")\n            return path_input or None\n        return files[choice].to_str()\n\n    def use_tk(self) -> bool:\n        return (\n            self.tk is not None\n            and self.filedialog is not None\n            and core.core_data.config.get_bool(core.ConfigKey.USE_FILE_DIALOG)\n        )\n\n    def get_file(\n        self,\n        title: str,\n        initialdir: str,\n        initialfile: str,\n        filetypes: list[tuple[str, str]] | None = None,\n        ignore_json: bool = False,\n    ) -> str | None:\n        if filetypes is None:\n            filetypes = []\n        title = core.core_data.local_manager.get_key(title)\n        color.ColoredText.localize(title)\n        if not self.use_tk():\n            curr_path = core.Path(initialdir).add(initialfile)\n            file = self.select_files_in_dir(curr_path.parent(), ignore_json)\n            if file is None:\n                return None\n            path_obj = core.Path(file)\n            if path_obj.exists():\n                return file\n            color.ColoredText.localize(\"path_not_exists\", path=path_obj)\n            return None\n\n        return (\n            self.filedialog.askopenfilename(  # type: ignore\n                title=title,\n                filetypes=filetypes,\n                initialdir=initialdir,\n                initialfile=initialfile,\n            )\n            or None\n        )\n\n    def save_file(\n        self,\n        title: str,\n        initialdir: str,\n        initialfile: str,\n        filetypes: list[tuple[str, str]] | None = None,\n    ) -> str | None:\n        \"\"\"Save file dialog\n\n        Args:\n            title (str): Title of dialog.\n            filetypes (list[tuple[str, str]] | None, optional): File types. Defaults to None.\n            initialdir (str, optional): Initial directory. Defaults to \"\".\n            initialfile (str, optional): Initial file. Defaults to \"\".\n\n        Returns:\n            str | None: Path to file.\n        \"\"\"\n        if filetypes is None:\n            filetypes = []\n        title = core.core_data.local_manager.get_key(title)\n        color.ColoredText.localize(title)\n        if not self.use_tk():\n            def_path = core.Path(initialdir).add(initialfile).to_str()\n            path = color.ColoredInput().localize(\n                \"enter_path_default\", default=def_path\n            )\n            return path.strip().strip(\"'\").strip('\"') if path else def_path\n        return (\n            self.filedialog.asksaveasfilename(  # type: ignore\n                title=title,\n                filetypes=filetypes,\n                initialdir=initialdir,\n                initialfile=initialfile,\n            )\n            or None\n        )\n"
  },
  {
    "path": "src/bcsfe/cli/main.py",
    "content": "from __future__ import annotations\n\n\"\"\"Main class for the CLI.\"\"\"\n\nimport sys\nimport traceback\nfrom typing import Any, NoReturn\nfrom bcsfe.cli import (\n    file_dialog,\n    color,\n    feature_handler,\n    save_management,\n    dialog_creator,\n)\nfrom bcsfe import core\n\n\nclass Main:\n    \"\"\"Main class for the CLI.\"\"\"\n\n    def __init__(self):\n        self.save_file = None\n        self.exit = False\n        self.save_path = None\n        self.fh = None\n\n    def wipe_temp_save(self):\n        \"\"\"Wipe the temp save.\"\"\"\n        core.SaveFile.get_temp_path().remove()\n\n    def main(self, input_path: str | None = None):\n        \"\"\"Main function for the CLI.\"\"\"\n        self.wipe_temp_save()\n        core.GameDataGetter.delete_old_versions(5)\n        self.check_update()\n        print()\n        self.print_start_text()\n        while not self.exit:\n            stop = self.load_save_options(input_path)\n            if stop:\n                break\n\n    def version_check(self, v1: str, v2: str) -> bool:\n        v1_p = v1.split(\".\")\n        v2_p = v2.split(\".\")\n\n        for p1, p2 in zip(v1_p, v2_p):\n            if p1.isdigit():\n                p1 = int(p1)\n            else:\n                continue\n            if p2.isdigit():\n                p2 = int(p2)\n            else:\n                continue\n            if p1 > p2:\n                return True\n            if p1 < p2:\n                return False\n\n        return len(v1_p) > len(v2_p)\n\n    def check_update(self):\n        \"\"\"Check for updates.\"\"\"\n\n        updater = core.Updater()\n        has_pre_release = updater.has_enabled_pre_release()\n        local_version = updater.get_local_version()\n        latest_version = updater.get_latest_version(has_pre_release)\n\n        if latest_version is None:\n            color.ColoredText.localize(\"update_check_fail\")\n            return\n\n        color.ColoredText.localize(\n            \"version_line\",\n            local_version=local_version,\n            latest_version=latest_version,\n        )\n\n        is_local_beta = \"b\" in local_version\n        is_latest_beta = \"b\" in latest_version\n\n        local_no_beta = local_version.split(\"b\")[0]\n        latest_no_beta = latest_version.split(\"b\")[0]\n\n        if self.version_check(latest_no_beta, local_no_beta):\n            update_needed = True\n        elif self.version_check(local_no_beta, latest_no_beta):\n            update_needed = False\n        else:\n            if latest_version == local_version:\n                update_needed = False\n            else:\n                if is_local_beta and is_latest_beta:\n                    update_needed = self.version_check(\n                        latest_version.replace(\"b\", \".\"),\n                        local_version.replace(\"b\", \".\"),\n                    )\n                elif is_local_beta:\n                    update_needed = True\n                else:\n                    update_needed = False\n\n        show_message = core.core_data.config.get(core.ConfigKey.SHOW_UPDATE_MESSAGE)\n        if not show_message:\n            update_needed = False\n\n        if update_needed:\n            update = dialog_creator.YesNoInput(True).get_input_once(\n                \"update_available\", {\"latest_version\": latest_version}\n            )\n            if update is None:\n                return\n\n            if update:\n                if updater.update(latest_version):\n                    color.ColoredText.localize(\"update_success\")\n                else:\n                    color.ColoredText.localize(\"update_fail\")\n                sys.exit()\n            else:\n                disable_message = dialog_creator.YesNoInput(False).get_input_once(\n                    \"disable_update_message\"\n                )\n                if disable_message is None:\n                    return\n\n                core.core_data.config.set(\n                    core.ConfigKey.SHOW_UPDATE_MESSAGE, not disable_message\n                )\n\n    def print_start_text(self):\n        external_theme = core.ExternalThemeManager.get_external_theme_config()\n        external_locale = core.ExternalLocaleManager.get_external_locale_config()\n        if external_theme is None:\n            theme_text = core.core_data.local_manager.get_key(\n                \"theme_text\",\n                theme_path=core.ThemeHandler.get_theme_path(\n                    core.core_data.theme_manager.theme_code\n                ),\n                theme_version=core.core_data.theme_manager.get_version(),\n                theme_author=core.core_data.theme_manager.get_author(),\n                theme_name=core.core_data.theme_manager.get_name(),\n                escape=False,\n            )\n        else:\n            theme_text = core.core_data.local_manager.get_key(\n                \"theme_text\",\n                theme_name=external_theme.name,\n                theme_version=external_theme.version,\n                theme_author=external_theme.author,\n                theme_path=core.ThemeHandler.get_theme_path(\n                    external_theme.get_full_name()\n                ),\n                escape=False,\n            )\n        if external_locale is None:\n            authors = core.core_data.local_manager.authors\n            locale_text = core.core_data.local_manager.get_key(\n                \"default_locale_text_authors\",\n                path=core.core_data.local_manager.path,\n                authors=\", \".join(authors),\n                name=core.core_data.local_manager.name,\n                escape=False,\n            )\n        else:\n            locale_text = core.core_data.local_manager.get_key(\n                \"locale_text\",\n                locale_name=external_locale.name,\n                locale_version=external_locale.version,\n                locale_author=external_locale.author,\n                locale_path=core.LocalManager.get_locale_folder(\n                    external_locale.get_full_name()\n                ),\n                escape=False,\n            )\n        color.ColoredText.localize(\n            \"welcome\",\n            config_path=core.core_data.config.get_config_path(),\n            locale_text=locale_text,\n            theme_text=theme_text,\n            escape=False,\n        )\n        print()\n\n    def load_save_options(self, input_path: str | None = None):\n        \"\"\"Load save options.\"\"\"\n        save_file, stop = save_management.SaveManagement.select_save(True, input_path)\n        if save_file is None:\n            return stop\n        self.save_file = save_file\n\n        color.ColoredText.localize(\n            \"current_save\",\n            inquiry_code=save_file.inquiry_code[:4]\n            + \"***\"\n            + save_file.inquiry_code[-2:],\n            gv=save_file.game_version,\n            cc=save_file.cc,\n        )\n\n        self.feature_handler()\n        return False\n\n    def feature_handler(self):\n        \"\"\"Run the feature handler.\"\"\"\n        if self.save_file is None:\n            return\n        self.fh = feature_handler.FeatureHandler(self.save_file)\n        self.fh.select_features_run()\n\n    @staticmethod\n    def save_save_dialog(save_file: core.SaveFile) -> core.Path | None:\n        \"\"\"Save save file dialog.\n\n        Args:\n            save_file (core.SaveFile): Save file to save.\n\n        Returns:\n            core.Path: Path to save file.\n        \"\"\"\n        path = file_dialog.FileDialog().save_file(\n            \"save_save_dialog\",\n            initialdir=core.SaveFile.get_saves_path().to_str(),\n            initialfile=\"SAVE_DATA\",\n        )\n        if path is None:\n            return None\n        path = core.Path(path)\n        path.parent().generate_dirs()\n        save_file.save_path = path\n        return path\n\n    @staticmethod\n    def save_json_dialog(json_data: dict[str, Any]) -> core.Path | None:\n        \"\"\"Save json file dialog.\n\n        Args:\n            json_data (dict): Json data to save.\n\n        Returns:\n            core.Path: Path to save file.\n        \"\"\"\n        path = file_dialog.FileDialog().save_file(\n            \"save_json_dialog\",\n            initialfile=\"SAVE_DATA.json\",\n            initialdir=core.SaveFile.get_saves_path().to_str(),\n        )\n        if path is None:\n            return None\n        path = core.Path(path)\n        path.parent().generate_dirs()\n        core.JsonFile.from_object(json_data).to_data().to_file(path)\n        return path\n\n    @staticmethod\n    def load_save_file() -> core.Path | None:\n        \"\"\"Load save file from file dialog.\n\n        Returns:\n            core.Path: Path to save file.\n        \"\"\"\n        path = file_dialog.FileDialog().get_file(\n            \"select_save_file\",\n            initialdir=core.SaveFile.get_saves_path().to_str(),\n            initialfile=\"SAVE_DATA\",\n            ignore_json=True,\n        )\n        if path is None:\n            return None\n        path = core.Path(path)\n        return path\n\n    @staticmethod\n    def load_save_data_json() -> tuple[core.Path, core.CountryCode] | None:\n        \"\"\"Load save data from json file.\n\n        Returns:\n            core.Path: Path to save file.\n        \"\"\"\n        path = file_dialog.FileDialog().get_file(\n            \"load_save_data_json\",\n            initialfile=\"SAVE_DATA.json\",\n            initialdir=core.SaveFile.get_saves_path().to_str(),\n        )\n        if path is None:\n            return None\n        path = core.Path(path)\n        if not path.exists():\n            return None\n        try:\n            json_data = core.JsonFile.from_data(path.read()).to_object()\n        except (core.JSONDecodeError, UnicodeDecodeError):\n            color.ColoredText.localize(\"parse_json_fail\")\n            return None\n        try:\n            save_file = core.SaveFile.from_dict(json_data)\n        except core.SaveError:\n            color.ColoredText.localize(\n                \"load_json_fail\", error=core.core_data.logger.get_traceback()\n            )\n            return None\n        path = Main.save_save_dialog(save_file)\n        if path is None:\n            return None\n        save_file.to_file(path)\n        return path, save_file.cc\n\n    @staticmethod\n    def exit_editor(\n        save_file: core.SaveFile | None = None, check_temp: bool = True\n    ) -> NoReturn:\n        \"\"\"Exit the editor.\"\"\"\n        save_file_temp = None\n        if check_temp:\n            temp_path = core.SaveFile.get_temp_path()\n            if temp_path.exists():\n                try:\n                    save_file_temp = core.SaveFile(temp_path.read())\n                except core.SaveError as e:\n                    tb = traceback.format_exc()\n                    color.ColoredText.localize(\n                        \"save_temp_fail\", error=str(e), traceback=tb\n                    )\n                    Main.leave()\n\n        if save_file is None:\n            save_file = save_file_temp\n        if save_file is None:\n            if check_temp:\n                color.ColoredText.localize(\"save_temp_not_found\")\n            Main.leave()\n        if save_file_temp is None:\n            save_file_temp = save_file\n\n        try:\n            print()\n            color.ColoredText.localize(\"checking_for_changes\")\n            if save_file.save_path is None:\n                same = False\n            else:\n                same = save_file.save_path.read() == save_file.to_data()\n        except core.SaveError:\n            same = False\n\n        if not same:\n            color.ColoredText.localize(\"changes_found\")\n            print()\n            save = color.ColoredInput().localize(\"save_before_exit\") == \"y\"\n            if save:\n                save_management.SaveManagement.save_save(save_file)\n        else:\n            color.ColoredText.localize(\"no_changes\")\n\n        Main.leave()\n\n    @staticmethod\n    def leave() -> NoReturn:\n        \"\"\"Leave the editor.\"\"\"\n        color.ColoredText.localize(\"leave\")\n        sys.exit()\n"
  },
  {
    "path": "src/bcsfe/cli/recent_saves.py",
    "content": "from __future__ import annotations\nfrom typing import Any\n\nfrom bcsfe import core\nimport datetime\nimport json\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass RecentSave:\n    def __init__(\n        self,\n        path: core.Path,\n        cc: core.CountryCode,\n        gv: core.GameVersion,\n        inquiry: str,\n        time: datetime.datetime,\n        name: core.Path,\n    ):\n        self.path = path\n        self.cc = cc\n        self.gv = gv\n        self.inquiry = inquiry\n        self.time = time\n        self.name = name\n\n    @staticmethod\n    def from_dict(data: dict[str, Any]) -> RecentSave | None:\n        path = data.get(\"path\")\n        cc = data.get(\"cc\")\n        gv = data.get(\"gv\")\n        inquiry = data.get(\"inquiry\")\n        time_stamp = data.get(\"timestamp\")\n        name = data.get(\"name\")\n        if (\n            path is None\n            or cc is None\n            or gv is None\n            or inquiry is None\n            or time_stamp is None\n            or name is None\n        ):\n            return None\n\n        return RecentSave(\n            core.Path(path),\n            core.CountryCode(cc),\n            core.GameVersion.from_string(gv),\n            inquiry,\n            datetime.datetime.fromtimestamp(time_stamp),\n            core.Path(name),\n        )\n\n    def to_dict(self) -> dict[str, Any]:\n        return {\n            \"path\": self.path.to_str(),\n            \"cc\": self.cc.get_code(),\n            \"gv\": self.gv.to_string(),\n            \"inquiry\": self.inquiry,\n            \"timestamp\": self.time.timestamp(),\n            \"name\": self.name.to_str(),\n        }\n\n\nclass RecentSaves:\n    def __init__(self, saves: list[RecentSave]):\n        self.saves = saves\n\n    @staticmethod\n    def from_json(data: list[dict[str, Any]]) -> RecentSaves:\n        res: list[RecentSave] = []\n\n        for item in data:\n            save = RecentSave.from_dict(item)\n            if save is not None:\n                res.append(save)\n\n        return RecentSaves(res)\n\n    def to_json(self) -> list[dict[str, Any]]:\n        return [save.to_dict() for save in self.saves][-10:]  # only store 10\n\n    @staticmethod\n    def from_path(path: core.Path) -> RecentSaves | None:\n        json_data = json.loads(path.read().to_str())\n\n        return RecentSaves.from_json(json_data)\n\n    def to_path(self, path: core.Path):\n        data = json.dumps(self.to_json(), indent=4)\n\n        try:\n            path.write(core.Data(data))\n        except Exception as e:\n            print(e)\n\n    @staticmethod\n    def read_default() -> RecentSaves:\n        path = RecentSaves.get_path()\n        if path.exists():\n            return RecentSaves.from_path(path) or RecentSaves([])\n        return RecentSaves([])\n\n    @staticmethod\n    def get_path() -> core.Path:\n        return core.Path.get_data_folder().add(\"recent_saves.json\")\n\n    def save_default(self):\n        path = RecentSaves.get_path()\n        self.to_path(path)\n\n    def select(self) -> RecentSave | None:\n        if not self.saves:\n            color.ColoredText.localize(\"no_recent_saves\")\n            return None\n        items: list[str] = []\n        for save in self.saves:\n            items.append(\n                core.localize(\n                    \"recent_save\",\n                    path=save.path,\n                    cc=save.cc,\n                    gv=save.gv,\n                    inquiry_code=save.inquiry,\n                    year=save.time.year,\n                    month=str(save.time.month).zfill(2),\n                    day=str(save.time.day).zfill(2),\n                    hour=str(save.time.hour).zfill(2),\n                    minute=str(save.time.minute).zfill(2),\n                    second=str(save.time.second).zfill(2),\n                    name=save.name,\n                )\n            )\n        items.reverse()\n\n        resp = dialog_creator.ChoiceInput.from_reduced(\n            items, localize_options=False, dialog=\"select_recent\"\n        ).single_choice()\n        if resp is None:\n            return None\n\n        resp = len(self.saves) - resp\n\n        return self.saves[resp]\n"
  },
  {
    "path": "src/bcsfe/cli/save_management.py",
    "content": "from __future__ import annotations\nimport datetime\nfrom bcsfe import core\nimport bcsfe\nfrom bcsfe.core import io\nfrom bcsfe.cli import main, color, dialog_creator, server_cli, recent_saves\nfrom bcsfe.core.country_code import CountryCode\nfrom bcsfe.core.io.config import ConfigKey\n\n\nclass SaveManagement:\n    def __init__(self):\n        pass\n\n    @staticmethod\n    def save_save(save_file: core.SaveFile, check_strict: bool = True):\n        \"\"\"Save the save file without a dialog.\n\n        Args:\n            save_file (core.SaveFile): The save file to save.\n        \"\"\"\n        SaveManagement.upload_items_checker(save_file, check_strict)\n\n        if save_file.save_path is None:\n            save_file.save_path = main.Main.save_save_dialog(save_file)\n\n        if save_file.save_path is None:\n            return\n\n        try:\n            save_file.to_file(save_file.save_path)\n        except OSError as e:\n            print(e)\n            return\n\n        color.ColoredText.localize(\"save_success\", path=save_file.save_path)\n\n    @staticmethod\n    def save_save_dialog(save_file: core.SaveFile):\n        \"\"\"Save the save file with a dialog.\n\n        Args:\n            save_file (core.SaveFile): The save file to save.\n        \"\"\"\n        SaveManagement.upload_items_checker(save_file)\n        save_file.save_path = main.Main.save_save_dialog(save_file)\n        if save_file.save_path is None:\n            return\n\n        save_file.to_file(save_file.save_path)\n\n        color.ColoredText.localize(\"save_success\", path=save_file.save_path)\n\n    @staticmethod\n    def save_save_data_dir(save_file: core.SaveFile):\n        \"\"\"Save the save file to the data folder.\n\n        Args:\n            save_file (core.SaveFile): The save file to save.\n        \"\"\"\n        SaveManagement.upload_items_checker(save_file)\n        save_file.save_path = core.SaveFile.get_save_path()\n        save_file.to_file(save_file.save_path)\n        color.ColoredText.localize(\"save_success\", path=save_file.save_path)\n\n    @staticmethod\n    def save_upload(save_file: core.SaveFile):\n        \"\"\"Save the save file and upload it to the server.\n\n        Args:\n            save_file (core.SaveFile): The save file to save.\n        \"\"\"\n        if core.core_data.config.get_bool(core.ConfigKey.STRICT_BAN_PREVENTION):\n            color.ColoredText.localize(\"strict_ban_prevention_enabled\")\n            SaveManagement.create_new_account(save_file)\n\n        result = core.ServerHandler(save_file).get_codes()\n        if result is not None:\n            SaveManagement.save_save(save_file, check_strict=False)\n            transfer_code, confirmation_code = result\n            color.ColoredText.localize(\n                \"upload_result\",\n                transfer_code=transfer_code,\n                confirmation_code=confirmation_code,\n            )\n        else:\n            color.ColoredText.localize(\"upload_fail\")\n            SaveManagement.save_save(save_file, check_strict=False)\n\n    @staticmethod\n    def unban_account(save_file: core.SaveFile):\n        \"\"\"Unban the account.\n\n        Args:\n            save_file (core.SaveFile): The save file to unban.\n        \"\"\"\n        server_handler = core.ServerHandler(save_file)\n        success = server_handler.create_new_account()\n        if success:\n            color.ColoredText.localize(\"unban_success\")\n        else:\n            color.ColoredText.localize(\"unban_fail\")\n\n    @staticmethod\n    def create_new_account(save_file: core.SaveFile):\n        \"\"\"Create a new account.\n\n        Args:\n            save_file (core.SaveFile): The save file to create a new account.\n        \"\"\"\n        server_handler = core.ServerHandler(save_file)\n        success = server_handler.create_new_account()\n        if success:\n            color.ColoredText.localize(\"create_new_account_success\")\n        else:\n            color.ColoredText.localize(\"create_new_account_fail\")\n\n    @staticmethod\n    def waydroid_push(save_file: core.SaveFile) -> core.WayDroidHandler | None:\n        SaveManagement.save_save(save_file)\n        try:\n            waydroid_handler = core.WayDroidHandler()\n        except core.AdbNotInstalled as e:\n            core.AdbHandler.display_no_adb_error(e)\n            return None\n        except core.io.waydroid.WayDroidNotInstalledError as e:\n            core.WayDroidHandler.display_waydroid_not_installed(e)\n            return None\n\n        if not waydroid_handler.adb_handler.select_device():\n            return None\n\n        if save_file.used_storage and save_file.package_name is not None:\n            waydroid_handler.set_package_name(save_file.package_name)\n        else:\n            packages = waydroid_handler.get_battlecats_packages()\n            package_name = SaveManagement.select_package_name(packages)\n            if package_name is None:\n                color.ColoredText.localize(\"no_package_name_error\")\n                return waydroid_handler\n            waydroid_handler.set_package_name(package_name)\n\n        if save_file.save_path is None:\n            return waydroid_handler\n\n        result = waydroid_handler.load_battlecats_save(save_file.save_path)\n        if result.success:\n            color.ColoredText.localize(\"waydroid_push_success\")\n        else:\n            color.ColoredText.localize(\"waydroid_push_fail\", error=result.result)\n\n        return waydroid_handler\n\n    @staticmethod\n    def waydroid_push_rerun(save_file: core.SaveFile) -> core.AdbHandler | None:\n        waydroid_handler = SaveManagement.waydroid_push(save_file)\n        if not waydroid_handler:\n            return\n        if waydroid_handler.package_name is None:\n            return\n        result = waydroid_handler.rerun_game()\n        if result.success:\n            color.ColoredText.localize(\"waydroid_rerun_success\")\n        else:\n            color.ColoredText.localize(\"waydroid_rerun_fail\", error=result.result)\n\n    @staticmethod\n    def adb_push(save_file: core.SaveFile) -> core.AdbHandler | None:\n        \"\"\"Push the save file to the device.\n\n        Args:\n            save_file (core.SaveFile): The save file to push.\n\n        Returns:\n            core.AdbHandler: The AdbHandler instance.\n        \"\"\"\n        SaveManagement.save_save(save_file)\n        try:\n            adb_handler = core.AdbHandler()\n        except core.AdbNotInstalled as e:\n            core.AdbHandler.display_no_adb_error(e)\n            return None\n        success = adb_handler.select_device()\n        if not success:\n            return adb_handler\n        if save_file.used_storage and save_file.package_name is not None:\n            adb_handler.set_package_name(save_file.package_name)\n        else:\n            packages = adb_handler.get_battlecats_packages()\n            package_name = SaveManagement.select_package_name(packages)\n            if package_name is None:\n                color.ColoredText.localize(\"no_package_name_error\")\n                return adb_handler\n            adb_handler.set_package_name(package_name)\n        if save_file.save_path is None:\n            return adb_handler\n        result = adb_handler.load_battlecats_save(save_file.save_path)\n        if result.success:\n            color.ColoredText.localize(\"adb_push_success\")\n        else:\n            color.ColoredText.localize(\"adb_push_fail\", error=result.result)\n\n        return adb_handler\n\n    @staticmethod\n    def root_push(save_file: core.SaveFile) -> core.RootHandler | None:\n        \"\"\"Push the save file to the device.\n\n        Args:\n            save_file (core.SaveFile): The save file to push.\n\n        Returns:\n            core.AdbHandler: The AdbHandler instance.\n        \"\"\"\n        SaveManagement.save_save(save_file)\n        root_handler = core.RootHandler()\n        if not root_handler.is_android():\n            color.ColoredText.localize(\"root_push_not_android_error\")\n            return None\n        if not root_handler.is_rooted():\n            color.ColoredText.localize(\"not_rooted_error\")\n            return None\n        if save_file.used_storage and save_file.package_name is not None:\n            root_handler.set_package_name(save_file.package_name)\n        else:\n            packages = root_handler.get_battlecats_packages()\n            package_name = SaveManagement.select_package_name(packages)\n            if package_name is None:\n                color.ColoredText.localize(\"no_package_name_error\")\n                return root_handler\n            root_handler.set_package_name(package_name)\n        if save_file.save_path is None:\n            return root_handler\n        result = root_handler.load_battlecats_save(save_file.save_path)\n        if result.success:\n            color.ColoredText.localize(\"root_push_success\")\n        else:\n            color.ColoredText.localize(\"root_push_fail\", error=result.result)\n\n        return root_handler\n\n    @staticmethod\n    def adb_push_rerun(save_file: core.SaveFile):\n        \"\"\"Push the save file to the device and rerun the game.\n\n        Args:\n            save_file (core.SaveFile): The save file to push.\n        \"\"\"\n        adb_handler = SaveManagement.adb_push(save_file)\n        if not adb_handler:\n            return\n        if adb_handler.package_name is None:\n            return\n        result = adb_handler.rerun_game()\n        if result.success:\n            color.ColoredText.localize(\"adb_rerun_success\")\n        else:\n            color.ColoredText.localize(\"adb_rerun_fail\", error=result.result)\n\n    @staticmethod\n    def root_push_rerun(save_file: core.SaveFile):\n        \"\"\"Push the save file to the device and rerun the game.\n\n        Args:\n            save_file (core.SaveFile): The save file to push.\n        \"\"\"\n        root_handler = SaveManagement.root_push(save_file)\n        if not root_handler:\n            return\n        if root_handler.package_name is None:\n            return\n        result = root_handler.rerun_game()\n        if result.success:\n            color.ColoredText.localize(\"root_rerun_success\")\n        else:\n            color.ColoredText.localize(\"root_rerun_fail\", error=result.result)\n\n    @staticmethod\n    def export_save(save_file: core.SaveFile):\n        \"\"\"Export the save file to a json file.\n\n        Args:\n            save_file (core.SaveFile): The save file to export.\n        \"\"\"\n        data = save_file.to_dict()\n        path = main.Main.save_json_dialog(data)\n        if path is None:\n            return\n        data = core.JsonFile.from_object(data).to_data()\n        data.to_file(path)\n        color.ColoredText.localize(\"export_success\", path=path)\n\n    @staticmethod\n    def init_save(save_file: core.SaveFile):\n        \"\"\"Initialize the save file to a new save file.\n\n        Args:\n            save_file (core.SaveFile): The save file to initialize.\n        \"\"\"\n        confirm = dialog_creator.YesNoInput().get_input_once(\"init_save_confirm\")\n        if not confirm:\n            return\n        save_file.init_save(save_file.game_version)\n        color.ColoredText.localize(\"init_save_success\")\n\n    @staticmethod\n    def upload_items(save_file: core.SaveFile, check_strict: bool = True):\n        \"\"\"Upload the items to the server.\n\n        Args:\n            save_file (core.SaveFile): The save file to upload.\n        \"\"\"\n        if (\n            core.core_data.config.get_bool(core.ConfigKey.STRICT_BAN_PREVENTION)\n            and check_strict\n        ):\n            color.ColoredText.localize(\"strict_ban_prevention_enabled\")\n            SaveManagement.create_new_account(save_file)\n\n        server_handler = core.ServerHandler(save_file)\n        success = server_handler.upload_meta_data()\n        if success:\n            color.ColoredText.localize(\"upload_items_success\")\n        else:\n            color.ColoredText.localize(\"upload_items_fail\")\n\n    @staticmethod\n    def upload_items_checker(save_file: core.SaveFile, check_strict: bool = True):\n        managed_items = core.BackupMetaData(save_file).get_managed_items()\n        if not managed_items:\n            return\n        should_upload = dialog_creator.YesNoInput().get_input_once(\n            \"upload_items_checker_confirm\"\n        )\n        if not should_upload:\n            return\n        SaveManagement.upload_items(save_file, check_strict)\n\n    @staticmethod\n    def select_save(\n        starting_options: bool = False, input_file: str | None = None\n    ) -> tuple[core.SaveFile | None, bool]:\n        \"\"\"Select a new save file.\n\n        Args:\n            starting_options (bool, optional): Whether to add the starting specific options. Defaults to False.\n\n\n        Returns:\n            core.SaveFile | None: The save file.\n        \"\"\"\n        if input_file is not None:\n            file = SaveManagement.load_save_file_path(\n                core.Path(input_file), None, False, None\n            )\n            if file is None:\n                return (None, True)\n            return (file[0], False)\n\n        options = [\n            \"download_save\",\n            \"select_save_file\",\n            core.localize(\"load_from_documents\", path=core.SaveFile.get_save_path()),\n            \"adb_pull_save\",\n            \"load_save_data_json\",\n        ]\n        if starting_options:\n            options.append(\"edit_config\")\n            options.append(\"update_external\")\n            options.append(\"exit\")\n\n        use_waydroid = core.core_data.config.get_bool(ConfigKey.USE_WAYDROID)\n        if use_waydroid:\n            options[3] = \"waydroid_pull_save\"\n\n        root_handler = io.root_handler.RootHandler()\n\n        if root_handler.is_android():\n            options[3] = \"root_storage_pull_save\"\n\n        choice = dialog_creator.ChoiceInput(\n            options, options, [], {}, \"save_load_option\", True\n        ).get_input_locale_while()\n        if choice is None:\n            return None, False\n        choice = choice[0] - 1\n\n        save_path = None\n        cc: core.CountryCode | None = None\n        used_storage = False\n        package_name = None\n\n        if choice == 0:\n            data = server_cli.ServerCLI().download_save()\n            if data is not None:\n                save_path, cc = data\n            else:\n                save_path = None\n        elif choice == 1:\n            save_path = main.Main.load_save_file()\n        elif choice == 2:\n            save_path = core.SaveFile.get_saves_path().add(\"SAVE_DATA\")\n            if not save_path.exists():\n                color.ColoredText.localize(\"save_file_not_found\")\n                return None, False\n        elif choice == 3:\n            handler = root_handler\n            if not root_handler.is_android():\n                if use_waydroid:\n                    try:\n                        handler = core.WayDroidHandler()\n                    except core.AdbNotInstalled as e:\n                        core.AdbHandler.display_no_adb_error(e)\n                        return None, False\n                    except core.io.waydroid.WayDroidNotInstalledError as e:\n                        core.WayDroidHandler.display_waydroid_not_installed(e)\n                        return None, False\n                    if not handler.adb_handler.select_device():\n                        return None, False\n                else:\n                    try:\n                        handler = core.AdbHandler()\n                    except core.AdbNotInstalled as e:\n                        core.AdbHandler.display_no_adb_error(e)\n                        return None, False\n                    if not handler.select_device():\n                        return None, False\n\n            elif not root_handler.is_rooted():\n                color.ColoredText.localize(\"not_rooted_error\")\n                return None, False\n\n            package_names = handler.get_battlecats_packages()\n\n            package_name = SaveManagement.select_package_name(package_names)\n            if package_name is None:\n                color.ColoredText.localize(\"no_package_name_error\")\n                return None, False\n            handler.set_package_name(package_name)\n            if root_handler.is_android():\n                key = \"storage_pulling\"\n            else:\n                if use_waydroid:\n                    key = \"waydroid_pulling\"\n                else:\n                    key = \"adb_pulling\"\n            color.ColoredText.localize(key, package_name=package_name)\n            save_path, result = handler.save_locally()\n            if save_path is None:\n                if root_handler.is_android():\n                    key = \"storage_pull_fail\"\n                else:\n                    if use_waydroid:\n                        key = \"waydroid_pull_fail\"\n                    else:\n                        key = \"adb_pull_fail\"\n                color.ColoredText.localize(\n                    key,\n                    package_name=package_name,\n                    error=result.result,\n                )\n            else:\n                used_storage = True\n        elif choice == 4:\n            data = main.Main.load_save_data_json()\n            if data is not None:\n                save_path, cc = data\n            else:\n                save_path = None\n        # elif choice == 5:\n        #     recent_save = recent_saves.RecentSaves.read_default().select()\n        #     if recent_save is None:\n        #         save_path = None\n        #     else:\n        #         save_path = recent_save.path\n        #         cc = recent_save.cc\n\n        # elif choice == 5:\n        #     color.ColoredText.localize(\"create_new_save_warning\")\n        #     cc = core.CountryCode.select()\n        #     if cc is None:\n        #         return None, False\n        #     try:\n        #         gv = core.GameVersion.from_string(\n        #             color.ColoredInput().localize(\n        #                 \"game_version_dialog\",\n        #             )\n        #         )\n        #     except ValueError:\n        #         color.ColoredText.localize(\"invalid_game_version\")\n        #         return None, False\n        #     save = core.SaveFile(cc=cc, gv=gv, load=False)\n        #     save_path = main.Main.save_save_dialog(save)\n        #     if save_path is None:\n        #         return None, False\n        #     save.to_file(save_path)\n        #     color.ColoredText.localize(\"create_new_save_success\")\n\n        elif choice == 5 and starting_options:\n            core.core_data.config.edit_config()\n        elif choice == 6 and starting_options:\n            core.update_external_content()\n        elif choice == 7 and starting_options:\n            main.Main.exit_editor(check_temp=False)\n\n        if save_path is None or not save_path.exists():\n            return None, False\n\n        save = SaveManagement.load_save_file_path(\n            save_path, cc, used_storage, package_name\n        )\n\n        if save is None:\n            return (None, False)\n\n        save, backup_path = save\n\n        if choice != 5:\n            recent_s = recent_saves.RecentSaves.read_default()\n            recent_s.saves.append(\n                recent_saves.RecentSave(\n                    backup_path,\n                    save.cc,\n                    save.game_version,\n                    save.inquiry_code,\n                    datetime.datetime.now(),\n                    save_path,\n                )\n            )\n            recent_s.save_default()\n        return (\n            save,\n            False,\n        )\n\n    @staticmethod\n    def load_save_file_path(\n        save_path: core.Path,\n        cc: CountryCode | None,\n        used_storage: bool,\n        package_name: str | None = None,\n    ) -> tuple[core.SaveFile, core.Path] | None:\n        color.ColoredText.localize(\"save_file_found\", path=save_path)\n\n        data = save_path.read()\n        try:\n            save_file = core.SaveFile(data, cc, package_name=package_name)\n        except core.CantDetectSaveCCError:\n            color.ColoredText.localize(\"cant_detect_cc\")\n            cc = core.CountryCode.select()\n            if cc is None:\n                return None\n            try:\n                save_file = core.SaveFile(data, cc)\n            except Exception:\n                tb = core.core_data.logger.get_traceback()\n                data.reset_pos()\n                color.ColoredText.localize(\n                    \"parse_save_error\",\n                    error=tb,\n                    version=bcsfe.__version__,\n                    game_version=data.read_int(),\n                    country_code=cc.get_code(),\n                )\n                return None\n\n        except Exception:\n            tb = core.core_data.logger.get_traceback()\n            save_file2 = core.SaveFile(data, cc, load=False)\n            data.reset_pos()\n            color.ColoredText.localize(\n                \"parse_save_error\",\n                error=tb,\n                version=bcsfe.__version__,\n                game_version=data.read_int(),\n                country_code=save_file2.cc,\n            )\n            return None\n\n        save_file.save_path = save_path\n        backup_path = save_file.get_default_path()\n        try:\n            save_file.save_path.copy_thread(backup_path)\n        except Exception as e:\n            print(e)\n        save_file.used_storage = used_storage\n\n        return save_file, backup_path\n\n    @staticmethod\n    def select_package_name(package_names: list[str]) -> str | None:\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            package_names,\n            dialog=\"select_package_name\",\n            single_choice=True,\n            localize_options=False,\n        ).single_choice()\n        if choice is None:\n            return None\n        return package_names[choice - 1]\n\n    @staticmethod\n    def load_save(save_file: core.SaveFile):\n        \"\"\"Load a new save file.\n\n        Args:\n            save_file (core.SaveFile): The current save file.\n        \"\"\"\n        SaveManagement.upload_items_checker(save_file)\n        new_save_file, stop = SaveManagement.select_save()\n        if new_save_file is None:\n            return stop\n        save_file.load_save_file(new_save_file)\n        core.core_data.init_data()\n        color.ColoredText.localize(\"load_save_success\")\n        return False\n\n    @staticmethod\n    def convert_save_cc(save_file: core.SaveFile):\n        color.ColoredText.localize(\"cc_warning\", current=save_file.cc)\n        ccs_to_select = core.CountryCode.get_all()\n        cc = core.CountryCode.select_from_ccs(ccs_to_select)\n        if cc is None:\n            return\n        save_file.set_cc(cc)\n        core.ServerHandler(save_file).create_new_account()\n        core.core_data.init_data()\n        color.ColoredText.localize(\"country_code_set\", cc=cc)\n\n    @staticmethod\n    def convert_save_gv(save_file: core.SaveFile):\n        color.ColoredText.localize(\n            \"gv_warning\", current=save_file.game_version.to_string()\n        )\n        try:\n            gv = core.GameVersion.from_string(\n                color.ColoredInput().localize(\"game_version_dialog\").strip()\n            )\n        except ValueError:\n            color.ColoredText.localize(\"invalid_game_version\")\n            return\n        save_file.set_gv(gv)\n        core.core_data.init_data()\n        color.ColoredText.localize(\"game_version_set\", version=gv.to_string())\n"
  },
  {
    "path": "src/bcsfe/cli/server_cli.py",
    "content": "from __future__ import annotations\nfrom bcsfe.cli import dialog_creator, main, color, file_dialog\nfrom bcsfe import core\n\n\nclass ServerCLI:\n    def __init__(self):\n        pass\n\n    def download_save(\n        self,\n    ) -> tuple[core.Path, core.CountryCode] | None:\n        transfer_code = dialog_creator.StringInput().get_input_locale_while(\n            \"enter_transfer_code\", {}\n        )\n        if transfer_code is None:\n            return None\n        confirmation_code = dialog_creator.StringInput().get_input_locale_while(\n            \"enter_confirmation_code\", {}\n        )\n        if confirmation_code is None:\n            return None\n        cc = core.CountryCode.select()\n        if cc is None:\n            return None\n        gv = core.GameVersion(120200)  # not important\n\n        color.ColoredText.localize(\n            \"downloading_save_file\",\n            transfer_code=transfer_code,\n            confirmation_code=confirmation_code,\n            country_code=cc,\n        )\n\n        server_handler, result = core.ServerHandler.from_codes(\n            transfer_code,\n            confirmation_code,\n            cc,\n            gv,\n        )\n        if server_handler is None and result is not None:\n            color.ColoredText.localize(\"invalid_codes_error\")\n            if dialog_creator.YesNoInput().get_input_once(\n                \"display_response_debug_info_q\"\n            ):\n                if result.response is not None:\n                    color.ColoredText.localize(\n                        \"response_text_display\",\n                        url=result.url,\n                        request_headers=result.headers,\n                        request_body=result.data,\n                        response_headers=result.response.headers,\n                        response_body=result.response.text,\n                    )\n            return\n        if server_handler is None:\n            return\n\n        save_file = server_handler.save_file\n        if file_dialog.FileDialog().filedialog is None:\n            path = core.SaveFile.get_saves_path().add(\"SAVE_DATA\")\n        else:\n            path = main.Main().save_save_dialog(save_file)\n        if path is None:\n            return None\n\n        try:\n            save_file.to_file(path)\n        except OSError as e:\n            print(\n                f\"failed to write save file to: {path} due to: {e}. Skipping writing the save file to disk\"\n            )\n            input(\"press enter to continue anyway\")\n\n        color.ColoredText.localize(\"save_downloaded\", path=path.to_str())\n\n        return path, cc\n"
  },
  {
    "path": "src/bcsfe/core/__init__.py",
    "content": "from __future__ import annotations\nfrom typing import Any\n\nfrom requests.exceptions import ConnectionError\nfrom requests import Response\nfrom json.decoder import JSONDecodeError\nfrom bcsfe.cli import color, dialog_creator\n\nfrom bcsfe.core import (\n    country_code,\n    crypto,\n    game,\n    game_version,\n    io,\n    locale_handler,\n    log,\n    server,\n    theme_handler,\n    max_value_helper,\n)\nfrom bcsfe.core.country_code import CountryCode, CountryCodeType\nfrom bcsfe.core.crypto import Hash, HashAlgorithm, Hmac, NyankoSignature, Random\nfrom bcsfe.core.game.battle.battle_items import BattleItems\nfrom bcsfe.core.game.battle.cleared_slots import ClearedSlots\nfrom bcsfe.core.game.battle.slots import LineUps\nfrom bcsfe.core.game.battle.enemy import (\n    Enemy,\n    EnemyNames,\n    EnemyDescriptions,\n    EnemyDictionary,\n)\nfrom bcsfe.core.game.catbase.beacon_base import BeaconEventListScene\nfrom bcsfe.core.game.catbase.cat import (\n    Cat,\n    Cats,\n    UnitBuy,\n    TalentData,\n    NyankoPictureBook,\n    StorageItem,\n)\nfrom bcsfe.core.game.catbase.gambling import GamblingEvent\nfrom bcsfe.core.game.catbase.gatya import (\n    Gatya,\n    GatyaInfos,\n    GatyaDataSet,\n    GatyaDataOptionSet,\n    GatyaDataOption,\n)\nfrom bcsfe.core.game.catbase.gatya_item import (\n    GatyaItemBuy,\n    GatyaItemNames,\n    GatyaItemCategory,\n    GatyaItemBuyItem,\n)\nfrom bcsfe.core.game.catbase.item_pack import (\n    ItemPack,\n    Purchases,\n    PurchaseSet,\n    PurchasedPack,\n)\nfrom bcsfe.core.game.catbase.login_bonuses import LoginBonus\nfrom bcsfe.core.game.catbase.matatabi import Matatabi\nfrom bcsfe.core.game.catbase.drop_chara import CharaDrop\nfrom bcsfe.core.game.catbase.medals import Medals, MedalNames\nfrom bcsfe.core.game.catbase.mission import (\n    Missions,\n    MissionNames,\n    MissionConditions,\n)\nfrom bcsfe.core.game.catbase.my_sale import MySale\nfrom bcsfe.core.game.catbase.nyanko_club import NyankoClub\nfrom bcsfe.core.game.catbase.officer_pass import OfficerPass\nfrom bcsfe.core.game.catbase.powerup import PowerUpHelper\nfrom bcsfe.core.game.catbase.scheme_items import SchemeItems\nfrom bcsfe.core.game.catbase.special_skill import (\n    SpecialSkills,\n    SpecialSkill,\n    AbilityData,\n    AbilityDataItem,\n)\n\nfrom bcsfe.core.game.catbase.stamp import StampData\nfrom bcsfe.core.game.catbase.talent_orbs import (\n    TalentOrb,\n    TalentOrbs,\n    OrbInfo,\n    OrbInfoList,\n    RawOrbInfo,\n    SaveOrb,\n    SaveOrbs,\n)\nfrom bcsfe.core.game.catbase.unlock_popups import (\n    UnlockPopups,\n    UnlockPopupData,\n    UnlockPopupLine,\n)\nfrom bcsfe.core.game.catbase.upgrade import Upgrade\nfrom bcsfe.core.game.catbase.user_rank_rewards import (\n    UserRankRewards,\n    RankGifts,\n    RankGiftDescriptions,\n)\nfrom bcsfe.core.game.catbase.playtime import PlayTime\nfrom bcsfe.core.game.gamoto.base_materials import BaseMaterials\nfrom bcsfe.core.game.gamoto.cat_shrine import CatShrine, CatShrineLevels\nfrom bcsfe.core.game.gamoto.gamatoto import (\n    Gamatoto,\n    GamatotoLevels,\n    GamatotoMembersName,\n)\nfrom bcsfe.core.game.gamoto.ototo import Ototo\nfrom bcsfe.core.game.localizable import Localizable\nfrom bcsfe.core.game.map.aku import AkuChapters\nfrom bcsfe.core.game.map.challenge import ChallengeChapters\nfrom bcsfe.core.game.map.chapters import Chapters\nfrom bcsfe.core.game.map.dojo import Dojo\nfrom bcsfe.core.game.map.enigma import Enigma\nfrom bcsfe.core.game.map.event import EventChapters\nfrom bcsfe.core.game.map.ex_stage import ExChapters\nfrom bcsfe.core.game.map.gauntlets import GauntletChapters\nfrom bcsfe.core.game.map.item_reward_stage import ItemRewardChapters\nfrom bcsfe.core.game.map.legend_quest import LegendQuestChapters\nfrom bcsfe.core.game.map.map_reset import MapResets\nfrom bcsfe.core.game.map.outbreaks import Outbreaks\nfrom bcsfe.core.game.map.story import StoryChapters, TreasureText, StageNames\nfrom bcsfe.core.game.map.timed_score import TimedScoreChapters\nfrom bcsfe.core.game.map.tower import TowerChapters\nfrom bcsfe.core.game.map.uncanny import UncannyChapters\nfrom bcsfe.core.game.map.zero_legends import ZeroLegendsChapters\nfrom bcsfe.core.game.map.map_names import MapNames\nfrom bcsfe.core.game.map.map_option import MapOption\nfrom bcsfe.core.game_version import GameVersion\nfrom bcsfe.core.io.adb_handler import AdbHandler, AdbNotInstalled\nfrom bcsfe.core.io.waydroid import WayDroidHandler\nfrom bcsfe.core.io.bc_csv import CSV, Delimeter, Row\nfrom bcsfe.core.io.command import Command, CommandResult\nfrom bcsfe.core.io.config import Config, ConfigKey\nfrom bcsfe.core.io.data import Data\nfrom bcsfe.core.io.json_file import JsonFile\nfrom bcsfe.core.io.path import Path\nfrom bcsfe.core.io.save import SaveError, SaveFile, CantDetectSaveCCError\nfrom bcsfe.core.io.thread_helper import thread_run_many, Thread\nfrom bcsfe.core.io.yaml import YamlFile\nfrom bcsfe.core.io.git_handler import GitHandler, Repo\nfrom bcsfe.core.io.root_handler import RootHandler\nfrom bcsfe.core.locale_handler import (\n    LocalManager,\n    ExternalLocaleManager,\n    ExternalLocale,\n)\nfrom bcsfe.core.log import Logger\nfrom bcsfe.core.server.event_data import (\n    ServerItemData,\n    ServerItemDataItem,\n    ServerGatyaData,\n    ServerGatyaDataSet,\n    ServerGatyaDataItem,\n)\nfrom bcsfe.core.server.client_info import ClientInfo\nfrom bcsfe.core.server.game_data_getter import GameDataGetter\nfrom bcsfe.core.server.headers import AccountHeaders\nfrom bcsfe.core.server.managed_item import (\n    BackupMetaData,\n    ManagedItem,\n    ManagedItemType,\n)\nfrom bcsfe.core.server.request import RequestHandler, MultiPartFile, MultipartForm\nfrom bcsfe.core.server.server_handler import ServerHandler\nfrom bcsfe.core.server.updater import Updater\nfrom bcsfe.core.theme_handler import (\n    ThemeHandler,\n    ExternalTheme,\n    ExternalThemeManager,\n)\nfrom bcsfe.core.max_value_helper import MaxValueHelper, MaxValueType\n\n\nclass CoreData:\n    def init_data(self):\n        self.config = Config(config_path, print_config_err)\n        self.logger = Logger(log_path)\n        self.local_manager = LocalManager()\n        self.theme_manager = ThemeHandler()\n        self.max_value_manager = MaxValueHelper()\n        self.game_data_getter: GameDataGetter | None = None\n        self.gatya_item_names: GatyaItemNames | None = None\n        self.gatya_item_buy: GatyaItemBuy | None = None\n        self.chara_drop: CharaDrop | None = None\n        self.gamatoto_levels: GamatotoLevels | None = None\n        self.gamatoto_members_name: GamatotoMembersName | None = None\n        self.localizable: Localizable | None = None\n        self.abilty_data: AbilityData | None = None\n        self.enemy_names: EnemyNames | None = None\n        self.rank_gift_descriptions: RankGiftDescriptions | None = None\n        self.rank_gifts: RankGifts | None = None\n        self.treasure_text: TreasureText | None = None\n        self.cat_shrine_levels: CatShrineLevels | None = None\n        self.medal_names: MedalNames | None = None\n        self.mission_names: MissionNames | None = None\n        self.mission_conditions: MissionConditions | None = None\n\n    def get_game_data_getter(\n        self,\n        save: SaveFile | None = None,\n        cc: CountryCode | None = None,\n        gv: GameVersion | None = None,\n    ) -> GameDataGetter:\n        if self.game_data_getter is None:\n            if cc is None and save is not None:\n                cc = save.cc\n            if cc is None:\n                raise ValueError(\"cc must be provided if save is not provided\")\n            if gv is None and save is not None:\n                gv = save.game_version\n            if gv is None:\n                raise ValueError(\"gv must be provided if save is not provided\")\n            self.game_data_getter = GameDataGetter(cc, gv)\n        return self.game_data_getter\n\n    def get_gatya_item_names(self, save: SaveFile) -> GatyaItemNames:\n        if self.gatya_item_names is None:\n            self.gatya_item_names = GatyaItemNames(save)\n        return self.gatya_item_names\n\n    def get_gatya_item_buy(self, save: SaveFile) -> GatyaItemBuy:\n        if self.gatya_item_buy is None:\n            self.gatya_item_buy = GatyaItemBuy(save)\n        return self.gatya_item_buy\n\n    def get_chara_drop(self, save: SaveFile) -> CharaDrop:\n        if self.chara_drop is None:\n            self.chara_drop = CharaDrop(save)\n        return self.chara_drop\n\n    def get_gamatoto_levels(self, save: SaveFile) -> GamatotoLevels:\n        if self.gamatoto_levels is None:\n            self.gamatoto_levels = GamatotoLevels(save)\n        return self.gamatoto_levels\n\n    def get_gamatoto_members_name(self, save: SaveFile) -> GamatotoMembersName:\n        if self.gamatoto_members_name is None:\n            self.gamatoto_members_name = GamatotoMembersName(save)\n        return self.gamatoto_members_name\n\n    def get_localizable(self, save: SaveFile) -> Localizable:\n        if self.localizable is None:\n            self.localizable = Localizable(save)\n        return self.localizable\n\n    def get_ability_data(self, save: SaveFile) -> AbilityData:\n        if self.abilty_data is None:\n            self.abilty_data = AbilityData(save)\n        return self.abilty_data\n\n    def get_enemy_names(self, save: SaveFile) -> EnemyNames:\n        if self.enemy_names is None:\n            self.enemy_names = EnemyNames(save)\n        return self.enemy_names\n\n    def get_rank_gift_descriptions(self, save: SaveFile) -> RankGiftDescriptions:\n        if self.rank_gift_descriptions is None:\n            self.rank_gift_descriptions = RankGiftDescriptions(save)\n        return self.rank_gift_descriptions\n\n    def get_rank_gifts(self, save: SaveFile) -> RankGifts:\n        if self.rank_gifts is None:\n            self.rank_gifts = RankGifts(save)\n        return self.rank_gifts\n\n    def get_treasure_text(self, save: SaveFile) -> TreasureText:\n        if self.treasure_text is None:\n            self.treasure_text = TreasureText(save)\n        return self.treasure_text\n\n    def get_cat_shrine_levels(self, save: SaveFile) -> CatShrineLevels:\n        if self.cat_shrine_levels is None:\n            self.cat_shrine_levels = CatShrineLevels(save)\n        return self.cat_shrine_levels\n\n    def get_medal_names(self, save: SaveFile) -> MedalNames:\n        if self.medal_names is None:\n            self.medal_names = MedalNames(save)\n        return self.medal_names\n\n    def get_mission_names(self, save: SaveFile) -> MissionNames:\n        if self.mission_names is None:\n            self.mission_names = MissionNames(save)\n        return self.mission_names\n\n    def get_mission_conditions(self, save: SaveFile) -> MissionConditions:\n        if self.mission_conditions is None:\n            self.mission_conditions = MissionConditions(save)\n        return self.mission_conditions\n\n    def get_lang(self, save: SaveFile) -> str:\n        return self.get_localizable(save).get_lang() or \"en\"\n\n\nconfig_path = None\nprint_config_err = True\nlog_path = None\ntransfer_backup_path = None\ngame_data_path = None\n\n\ndef set_config_path(path: Path):\n    global config_path\n    config_path = path\n\n\ndef set_game_data_path(path: Path):\n    global game_data_path\n    game_data_path = path\n\n\ndef set_log_path(path: Path):\n    global log_path\n    log_path = path\n\n\ndef set_transfer_backup_path(path: Path):\n    global transfer_backup_path\n    transfer_backup_path = path\n\n\ndef get_transfer_backup_path() -> Path | None:\n    return transfer_backup_path\n\n\ndef get_game_data_path() -> Path | None:\n    return game_data_path\n\n\ndef update_external_content(_: Any = None):\n    \"\"\"Updates external content.\"\"\"\n\n    color.ColoredText.localize(\"updating_external_content\")\n    print()\n    ExternalThemeManager.update_all_external_themes()\n    ExternalLocaleManager.update_all_external_locales()\n    core_data.init_data()\n\n    clear_game_data = dialog_creator.YesNoInput().get_input_once(\"clear_game_data_q\")\n    if clear_game_data is None:\n        return\n\n    if clear_game_data:\n        GameDataGetter.delete_old_versions(0)\n        color.ColoredText.localize(\"cleared_game_data\")\n\n\ndef print_no_internet():\n    color.ColoredText.localize(\"no_internet\")\n\n\ncore_data = CoreData()\n\n\ndef localize(key: str, escape: bool = True, **kwargs: Any) -> str:\n    return core_data.local_manager.get_key(key, escape=escape, **kwargs)\n\n\n__all__ = [\n    \"server\",\n    \"io\",\n    \"locale_handler\",\n    \"country_code\",\n    \"log\",\n    \"game_version\",\n    \"crypto\",\n    \"game\",\n    \"theme_handler\",\n    \"max_value_helper\",\n    \"AdbHandler\",\n    \"AdbNotInstalled\",\n    \"CountryCode\",\n    \"Path\",\n    \"Data\",\n    \"CSV\",\n    \"ServerHandler\",\n    \"GameVersion\",\n    \"SaveFile\",\n    \"JsonFile\",\n    \"ManagedItem\",\n    \"ManagedItemType\",\n    \"BackupMetaData\",\n    \"Cat\",\n    \"Upgrade\",\n    \"PowerUpHelper\",\n    \"TalentOrb\",\n    \"TalentOrbs\",\n    \"OrbInfo\",\n    \"OrbInfoList\",\n    \"RawOrbInfo\",\n    \"SaveOrb\",\n    \"SaveOrbs\",\n    \"ConfigKey\",\n    \"SpecialSkill\",\n    \"WayDroidHandler\",\n    \"EnemyDescriptions\",\n    \"EnemyDictionary\",\n    \"GatyaItemCategory\",\n    \"ServerItemData\",\n    \"GatyaItemBuyItem\",\n    \"ServerItemDataItem\",\n    \"ServerGatyaData\",\n    \"ServerGatyaDataSet\",\n    \"ServerGatyaDataItem\",\n    \"GatyaDataOptionSet\",\n    \"GatyaDataOption\",\n    \"MaxValueType\",\n    \"GamblingEvent\",\n    \"UnitBuy\",\n    \"NyankoPictureBook\",\n    \"StorageItem\",\n    \"OfficerPass\",\n    \"LocalManager\",\n    \"MapOption\",\n    \"CantDetectSaveCCError\",\n    \"UnlockPopupData\",\n    \"UnlockPopupLine\",\n]\n"
  },
  {
    "path": "src/bcsfe/core/country_code.py",
    "content": "from __future__ import annotations\nimport enum\nfrom bcsfe.cli import dialog_creator\nfrom bcsfe import core\n\n\nclass CountryCodeType(enum.Enum):\n    EN = \"en\"\n    JP = \"jp\"\n    KR = \"kr\"\n    TW = \"tw\"\n\n\nclass CountryCode:\n    def __init__(self, cc: str | CountryCodeType):\n        self.value = cc.value if isinstance(cc, CountryCodeType) else cc\n        self.value = self.value.lower()\n\n    def get_code(self) -> str:\n        return self.value\n\n    def get_client_info_code(self) -> str:\n        code = self.get_code()\n        if code == \"jp\":\n            return \"ja\"\n        return code\n\n    def get_patching_code(self) -> str:\n        code = self.get_code()\n        if code == \"jp\":\n            return \"\"\n        return code\n\n    @staticmethod\n    def from_patching_code(code: str) -> CountryCode:\n        if code == \"\":\n            return CountryCode(CountryCodeType.JP)\n        return CountryCode(code)\n\n    @staticmethod\n    def from_code(code: str) -> CountryCode:\n        return CountryCode(code)\n\n    @staticmethod\n    def get_all() -> list[\"CountryCode\"]:\n        return [CountryCode(cc) for cc in CountryCodeType]\n\n    @staticmethod\n    def get_all_str() -> list[str]:\n        ccts = CountryCode.get_all()\n        return [cc.get_code() for cc in ccts]\n\n    def __str__(self) -> str:\n        return self.get_code()\n\n    def __repr__(self) -> str:\n        return self.get_code()\n\n    def copy(self) -> CountryCode:\n        return self\n\n    @staticmethod\n    def select() -> CountryCode | None:\n        index = dialog_creator.ChoiceInput.from_reduced(\n            CountryCode.get_all_str(),\n            dialog=\"country_code_select\",\n            single_choice=True,\n        ).single_choice()\n        if index is None:\n            return None\n        return CountryCode.get_all()[index - 1]\n\n    @staticmethod\n    def select_from_ccs(ccs: list[CountryCode]) -> CountryCode | None:\n        index = dialog_creator.ChoiceInput.from_reduced(\n            [cc.get_code() for cc in ccs],\n            dialog=\"country_code_select\",\n            single_choice=True,\n        ).single_choice()\n        if index is None:\n            return None\n        return ccs[index - 1]\n\n    def __eq__(self, o: object) -> bool:\n        if isinstance(o, CountryCode):\n            return self.get_code() == o.get_code()\n        elif isinstance(o, str):\n            return self.get_code() == o\n        elif isinstance(o, CountryCodeType):\n            return self.get_code() == o.value\n        return False\n\n    def get_cc_lang(self) -> core.CountryCode:\n        if core.core_data.config.get_bool(core.ConfigKey.FORCE_LANG_GAME_DATA):\n            locale = core.core_data.config.get_str(core.ConfigKey.LOCALE)\n            return core.CountryCode.from_code(locale)\n        return self\n\n    @staticmethod\n    def get_langs() -> list[str]:\n        return [\"de\", \"it\", \"es\", \"fr\", \"th\"]\n\n    def is_lang(self) -> bool:\n        return self.get_code() in CountryCode.get_langs()\n"
  },
  {
    "path": "src/bcsfe/core/crypto.py",
    "content": "from __future__ import annotations\nimport enum\nimport hashlib\nimport hmac\nimport random\nfrom bcsfe import core\n\n\nclass HashAlgorithm(enum.Enum):\n    \"\"\"An enum representing a hash algorithm.\"\"\"\n\n    MD5 = enum.auto()\n    SHA1 = enum.auto()\n    SHA256 = enum.auto()\n\n\nclass Hash:\n    \"\"\"A class to hash data.\"\"\"\n\n    def __init__(self, algorithm: HashAlgorithm):\n        \"\"\"Initializes a new instance of the Hash class.\n\n        Args:\n            algorithm (HashAlgorithm): The hash algorithm to use.\n        \"\"\"\n        self.algorithm = algorithm\n\n    def get_hash(\n        self,\n        data: core.Data,\n        length: int | None = None,\n    ) -> core.Data:\n        \"\"\"Gets the hash of the given data.\n\n        Args:\n            data (core.Data): The data to hash.\n            length (int | None, optional): The length of the hash. Defaults to None.\n\n        Raises:\n            ValueError: Invalid hash algorithm.\n\n        Returns:\n            core.Data: The hash of the data.\n        \"\"\"\n        if self.algorithm == HashAlgorithm.MD5:\n            hash = hashlib.md5()\n        elif self.algorithm == HashAlgorithm.SHA1:\n            hash = hashlib.sha1()\n        elif self.algorithm == HashAlgorithm.SHA256:\n            hash = hashlib.sha256()\n        else:\n            raise ValueError(\"Invalid hash algorithm\")\n        hash.update(data.get_bytes())\n        if length is None:\n            return core.Data(hash.digest())\n        return core.Data(hash.digest()[:length])\n\n\nclass Random:\n    \"\"\"A class to get random data\"\"\"\n\n    @staticmethod\n    def get_bytes(length: int) -> bytes:\n        \"\"\"Gets random bytes.\n\n        Args:\n            length (int): The length of the bytes.\n\n        Returns:\n            bytes: The random bytes.\n        \"\"\"\n        return bytes(random.getrandbits(8) for _ in range(length))\n\n    @staticmethod\n    def get_alpha_string(length: int) -> str:\n        \"\"\"Gets a random string of the given length.\n\n        Args:\n            length (int): The length of the string.\n\n        Returns:\n            str: The random string.\n        \"\"\"\n        characters = \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n        return \"\".join(random.choice(characters) for _ in range(length))\n\n    @staticmethod\n    def get_hex_string(length: int) -> str:\n        \"\"\"Gets a random hex string of the given length.\n\n        Args:\n            length (int): The length of the string.\n\n        Returns:\n            str: The random string.\n        \"\"\"\n        characters = \"0123456789abcdef\"\n        return \"\".join(random.choice(characters) for _ in range(length))\n\n    @staticmethod\n    def get_digits_string(length: int) -> str:\n        \"\"\"Gets a random digits string of the given length.\n\n        Args:\n            length (int): The length of the string.\n\n        Returns:\n            str: The random string.\n        \"\"\"\n        characters = \"0123456789\"\n        return \"\".join(random.choice(characters) for _ in range(length))\n\n\nclass Hmac:\n    def __init__(self, algorithm: HashAlgorithm):\n        self.algorithm = algorithm\n\n    def get_hmac(self, key: core.Data, data: core.Data) -> core.Data:\n        if self.algorithm == HashAlgorithm.MD5:\n            alg = hashlib.md5\n        elif self.algorithm == HashAlgorithm.SHA1:\n            alg = hashlib.sha1\n        elif self.algorithm == HashAlgorithm.SHA256:\n            alg = hashlib.sha256\n        else:\n            raise ValueError(\"Invalid hash algorithm\")\n        hmac_data = hmac.new(\n            key.get_bytes(), data.get_bytes(), digestmod=alg\n        ).digest()\n        return core.Data(hmac_data)\n\n\nclass NyankoSignature:\n    def __init__(self, inquiry_code: str, data: str):\n        self.inquiry_code = inquiry_code\n        self.data = data\n\n    def generate_signature(self) -> str:\n        \"\"\"Generates a signature from the inquiry code and data.\n\n        Returns:\n            str: The signature.\n        \"\"\"\n        random_data = Random.get_hex_string(64)\n        key = self.inquiry_code + random_data\n        hmac_ = Hmac(HashAlgorithm.SHA256)\n        signature = hmac_.get_hmac(core.Data(key), core.Data(self.data))\n\n        return random_data + signature.to_hex()\n\n    def generate_signature_v1(self) -> str:\n        \"\"\"Generates a signature from the inquiry code and data.\n\n        Returns:\n            str: The signature.\n        \"\"\"\n\n        data = self.data + self.data  # repeat data for some reason\n        random_data = Random.get_hex_string(40)\n        key = self.inquiry_code + random_data\n        hmac_ = Hmac(HashAlgorithm.SHA1)\n        signature = hmac_.get_hmac(core.Data(key), core.Data(data))\n\n        return random_data + signature.to_hex()\n"
  },
  {
    "path": "src/bcsfe/core/game/__init__.py",
    "content": "from bcsfe.core.game import catbase, battle, map, gamoto, localizable\n\n__all__ = [\"catbase\", \"battle\", \"map\", \"gamoto\", \"localizable\"]\n"
  },
  {
    "path": "src/bcsfe/core/game/battle/__init__.py",
    "content": "from bcsfe.core.game.battle import slots, battle_items, cleared_slots\n\n__all__ = [\"slots\", \"battle_items\", \"cleared_slots\"]\n"
  },
  {
    "path": "src/bcsfe/core/game/battle/battle_items.py",
    "content": "from __future__ import annotations\n\nimport datetime\nfrom math import inf, isnan\nimport math\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass EndlessItem:\n    def __init__(\n        self, active: bool, unknown: bool, amount: int, start: float, end: float\n    ):\n        self.active = active\n        self.unknown = unknown\n        self.amount = amount\n        self.start = start\n        self.end = end\n\n    @staticmethod\n    def init() -> EndlessItem:\n        return EndlessItem(False, False, 0, 0, 0)\n\n    @staticmethod\n    def read(stream: core.Data) -> EndlessItem:\n        return EndlessItem(\n            stream.read_bool(),\n            stream.read_bool(),\n            stream.read_byte(),\n            stream.read_double(),\n            stream.read_double(),\n        )\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.active)\n        stream.write_bool(self.unknown)\n        stream.write_byte(self.amount)\n        stream.write_double(self.start)\n        stream.write_double(self.end)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"active\": self.active,\n            \"unknown\": self.unknown,\n            \"amount\": self.amount,\n            \"start\": self.start,\n            \"end\": self.end,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> EndlessItem:\n        return EndlessItem(\n            data.get(\"active\", False),\n            data.get(\"unknown\", False),\n            data.get(\"amount\", 0),\n            data.get(\"start\", 0.0),\n            data.get(\"end\", 0.0),\n        )\n\n    def get_endless_duration(self) -> datetime.timedelta | None:\n        if not self.active:\n            return datetime.timedelta()\n\n        if self.end == inf:\n            return None\n        if math.isnan(self.end) or math.isnan(self.start):\n            return None\n\n        return datetime.timedelta(\n            seconds=self.end - self.start + (self.amount * 3 * 60 * 60)\n        )\n\n    def get_endless_duration_formatted(self) -> str:\n        duration = self.get_endless_duration()\n\n        if duration is None:\n            return core.localize(\"infinity_duration\")\n\n        days = duration.days\n        hours, rem = divmod(duration.seconds, 3600)\n        minutes, seconds = divmod(rem, 60)\n\n        return core.localize(\n            \"duration\", days=days, hours=hours, minutes=minutes, seconds=seconds\n        )\n\n    def set_duration_mins(self, mins: float, amount: int):\n        self.active = True\n        self.unknown = True\n        self.amount = amount\n        self.start = datetime.datetime.now(datetime.timezone.utc).timestamp()\n        self.end = self.start + mins * 60\n\n\nclass BattleItem:\n    def __init__(self, amount: int):\n        self.amount = amount\n        self.locked = False\n\n        self.endless_item = EndlessItem.init()\n\n    @staticmethod\n    def init() -> BattleItem:\n        return BattleItem(0)\n\n    @staticmethod\n    def read_amount(stream: core.Data) -> BattleItem:\n        return BattleItem(stream.read_int())\n\n    def write_amount(self, stream: core.Data):\n        stream.write_int(self.amount)\n\n    def read_locked(self, stream: core.Data):\n        self.locked = stream.read_bool()\n\n    def write_locked(self, stream: core.Data):\n        stream.write_bool(self.locked)\n\n    def read_endless_items(self, stream: core.Data):\n        self.endless_item = EndlessItem.read(stream)\n\n    def write_endless_items(self, stream: core.Data):\n        self.endless_item.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"amount\": self.amount,\n            \"locked\": self.locked,\n            \"endless\": self.endless_item.serialize(),\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> BattleItem:\n        battle_item = BattleItem(data.get(\"amount\", 0))\n        battle_item.locked = data.get(\"locked\", False)\n        battle_item.endless_item = EndlessItem.deserialize(data.get(\"endless\", {}))\n        return battle_item\n\n    def __repr__(self):\n        try:\n            return f\"BattleItem({self.amount}, {self.locked}, {self.endless_item})\"\n        except AttributeError:\n            return f\"BattleItem({self.amount}, {self.endless_item})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass BattleItems:\n    def __init__(self, items: list[BattleItem]):\n        self.items = items\n        self.lock_item = False\n\n    @staticmethod\n    def init() -> BattleItems:\n        return BattleItems([BattleItem.init() for _ in range(6)])\n\n    @staticmethod\n    def read_items(stream: core.Data) -> BattleItems:\n        total_items = 6\n        items = [BattleItem.read_amount(stream) for _ in range(total_items)]\n        return BattleItems(items)\n\n    def write_items(self, stream: core.Data):\n        for item in self.items:\n            item.write_amount(stream)\n\n    def read_locked_items(self, stream: core.Data):\n        self.lock_item = stream.read_bool()\n        for item in self.items:\n            item.read_locked(stream)\n\n    def write_locked_items(self, stream: core.Data):\n        stream.write_bool(self.lock_item)\n        for item in self.items:\n            item.write_locked(stream)\n\n    def read_endless_items(self, stream: core.Data):\n        for i in range(6):\n            if i >= len(self.items):\n                _ = EndlessItem.read(stream)  # ensure we still read 6 items\n            else:\n                item = self.items[i]\n                item.read_endless_items(stream)\n\n    def write_endless_items(self, stream: core.Data):\n        for i in range(6):\n            if i >= len(self.items):\n                EndlessItem.init().write(stream)  # ensure we still write 6 items\n            else:\n                item = self.items[i]\n                item.write_endless_items(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"items\": [item.serialize() for item in self.items],\n            \"lock_item\": self.lock_item,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> BattleItems:\n        battle_items = BattleItems(\n            [BattleItem.deserialize(item) for item in data.get(\"items\", [])]\n        )\n        battle_items.lock_item = data.get(\"lock_item\", False)\n        return battle_items\n\n    def __repr__(self):\n        return f\"BattleItems({self.items})\"\n\n    def __str__(self):\n        return f\"BattleItems({self.items})\"\n\n    def get_names(self, save_file: core.SaveFile) -> list[str] | None:\n        names = core.core_data.get_gatya_item_names(save_file).names\n        if names is None:\n            return None\n        items = core.core_data.get_gatya_item_buy(save_file).get_by_category(3)\n        if items is None:\n            return None\n\n        names = [names[item.id] for item in items]\n        return names\n\n    def edit(self, save_file: core.SaveFile):\n        group_name = save_file.get_localizable().get(\"shop_category1\")\n        if group_name is None:\n            group_name = core.core_data.local_manager.get_key(\"battle_items\")\n        item_names = self.get_names(save_file)\n        if item_names is None:\n            return\n        current_values = [item.amount for item in self.items]\n        values = dialog_creator.MultiEditor.from_reduced(\n            group_name,\n            item_names,\n            current_values,\n            core.core_data.max_value_manager.get(\"battle_items\"),\n        ).edit()\n        for i, value in enumerate(values):\n            self.items[i].amount = value\n\n    def edit_endless_items(self, save_file: core.SaveFile):\n        item_names = self.get_names(save_file)\n        if item_names is None:\n            return\n\n        current_values = [\n            item.endless_item.get_endless_duration_formatted() for item in self.items\n        ]\n\n        (options, all_at_once) = dialog_creator.ChoiceInput.from_reduced(\n            [core.localize(\"endless_item_item\", item=item) for item in item_names],\n            current_values,\n            localize_options=False,\n            dialog=\"select_option\",\n        ).multiple_choice(False)\n\n        if options is None:\n            return\n\n        infinity_str = core.localize(\"infinity\")\n\n        if all_at_once:\n            val = dialog_creator.StringInput().get_input_locale_while(\n                \"enter_duration_minutes\", {}\n            )\n            if val is None:\n                return\n\n            if val.lower() == infinity_str.lower():\n                val = inf\n            else:\n                try:\n                    val = float(val)\n                except ValueError:\n                    return\n\n            for item in self.items:\n                item.endless_item.set_duration_mins(val, 0)\n        else:\n            for opt in options:\n                val = dialog_creator.StringInput().get_input_locale_while(\n                    \"enter_duration_minutes_item\", {\"item\": item_names[opt]}\n                )\n                if val is None:\n                    return\n\n                if val.lower() == infinity_str.lower():\n                    val = inf\n                else:\n                    try:\n                        val = float(val)\n                    except ValueError:\n                        color.ColoredText.localize(\"invalid_minute_count\")\n                        continue\n\n                self.items[opt].endless_item.set_duration_mins(val, 0)\n\n        color.ColoredText.localize(\"endless_items_success\")\n"
  },
  {
    "path": "src/bcsfe/core/game/battle/cleared_slots.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom typing import Any\n\n\nclass CatSlot:\n    def __init__(self, cat_id: int, form: int):\n        self.cat_id = cat_id\n        self.form = form\n\n    @staticmethod\n    def init() -> CatSlot:\n        return CatSlot(0, 0)\n\n    @staticmethod\n    def read(stream: core.Data) -> CatSlot:\n        cat_id = stream.read_short()\n        form = stream.read_byte()\n        return CatSlot(cat_id, form)\n\n    def write(self, stream: core.Data):\n        stream.write_short(self.cat_id)\n        stream.write_byte(self.form)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"cat_id\": self.cat_id,\n            \"form\": self.form,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> CatSlot:\n        return CatSlot(data.get(\"cat_id\", 0), data.get(\"form\", 0))\n\n    def __repr__(self):\n        return f\"CatSlot({self.cat_id}, {self.form})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass LineupCat:\n    def __init__(\n        self,\n        index: int,\n        cats: list[CatSlot],\n        u1: int,\n        u2: int,\n        u3: int,\n    ):\n        self.index = index\n        self.cats = cats\n        self.u1 = u1\n        self.u2 = u2\n        self.u3 = u3\n\n    @staticmethod\n    def init() -> LineupCat:\n        cats = [CatSlot.init() for _ in range(10)]\n        return LineupCat(0, cats, 0, 0, 0)\n\n    @staticmethod\n    def read(stream: core.Data) -> LineupCat:\n        index = stream.read_short()\n        length = 10\n\n        cats = [CatSlot.read(stream) for _ in range(length)]\n        u1 = stream.read_byte()\n        u2 = stream.read_byte()\n        u3 = stream.read_byte()\n        return LineupCat(index, cats, u1, u2, u3)\n\n    def write(self, stream: core.Data):\n        stream.write_short(self.index)\n        for cat in self.cats:\n            cat.write(stream)\n        stream.write_byte(self.u1)\n        stream.write_byte(self.u2)\n        stream.write_byte(self.u3)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"index\": self.index,\n            \"cats\": [cat.serialize() for cat in self.cats],\n            \"u1\": self.u1,\n            \"u2\": self.u2,\n            \"u3\": self.u3,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> LineupCat:\n        return LineupCat(\n            data.get(\"index\", 0),\n            [CatSlot.deserialize(cat) for cat in data.get(\"cats\", [])],\n            data.get(\"u1\", 0),\n            data.get(\"u2\", 0),\n            data.get(\"u3\", 0),\n        )\n\n    def __repr__(self):\n        return f\"LineupCat({self.index}, {self.cats}, {self.u1}, {self.u2}, {self.u3})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ClearedSlotsCat:\n    def __init__(self, lineups: list[LineupCat]):\n        self.lineups = lineups\n\n    @staticmethod\n    def init() -> ClearedSlotsCat:\n        return ClearedSlotsCat([])\n\n    @staticmethod\n    def read(stream: core.Data) -> ClearedSlotsCat:\n        total = stream.read_short()\n        lineups = [LineupCat.read(stream) for _ in range(total)]\n        return ClearedSlotsCat(lineups)\n\n    def write(self, stream: core.Data):\n        stream.write_short(len(self.lineups))\n        for lineup in self.lineups:\n            lineup.write(stream)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [lineup.serialize() for lineup in self.lineups]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> ClearedSlotsCat:\n        return ClearedSlotsCat(\n            [LineupCat.deserialize(lineup) for lineup in data],\n        )\n\n    def __repr__(self):\n        return f\"ClearedSlotsCat({self.lineups})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass StageSlot:\n    def __init__(self, stage_id: int):\n        self.stage_id = stage_id\n\n    @staticmethod\n    def init() -> StageSlot:\n        return StageSlot(0)\n\n    @staticmethod\n    def read(stream: core.Data) -> StageSlot:\n        stage_id = stream.read_int()\n        return StageSlot(stage_id)\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.stage_id)\n\n    def serialize(self) -> int:\n        return self.stage_id\n\n    @staticmethod\n    def deserialize(data: int) -> StageSlot:\n        return StageSlot(data)\n\n    def __repr__(self):\n        return f\"StageSlot({self.stage_id})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass StageLineups:\n    def __init__(self, index: int, slots: list[StageSlot]):\n        self.index = index\n        self.slots = slots\n\n    @staticmethod\n    def init() -> StageLineups:\n        return StageLineups(0, [])\n\n    @staticmethod\n    def read(stream: core.Data) -> StageLineups:\n        index = stream.read_short()\n        total = stream.read_short()\n        slots = [StageSlot.read(stream) for _ in range(total)]\n        return StageLineups(index, slots)\n\n    def write(self, stream: core.Data):\n        stream.write_short(self.index)\n        stream.write_short(len(self.slots))\n        for slot in self.slots:\n            slot.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"index\": self.index,\n            \"slots\": [slot.serialize() for slot in self.slots],\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> StageLineups:\n        return StageLineups(\n            data.get(\"index\", 0),\n            [StageSlot.deserialize(slot) for slot in data.get(\"slots\", [])],\n        )\n\n    def __repr__(self):\n        return f\"StageLineups({self.index}, {self.slots})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ClearedStageSlots:\n    def __init__(self, lineups: list[StageLineups]):\n        self.lineups = lineups\n\n    @staticmethod\n    def init() -> ClearedStageSlots:\n        return ClearedStageSlots([])\n\n    @staticmethod\n    def read(stream: core.Data) -> ClearedStageSlots:\n        total = stream.read_short()\n        lineups = [StageLineups.read(stream) for _ in range(total)]\n        return ClearedStageSlots(lineups)\n\n    def write(self, stream: core.Data):\n        stream.write_short(len(self.lineups))\n        for lineup in self.lineups:\n            lineup.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"lineups\": [lineup.serialize() for lineup in self.lineups],\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> ClearedStageSlots:\n        return ClearedStageSlots(\n            [\n                StageLineups.deserialize(lineup)\n                for lineup in data.get(\"lineups\", [])\n            ],\n        )\n\n    def __repr__(self):\n        return f\"ClearedStageSlots({self.lineups})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ClearedSlots:\n    def __init__(\n        self,\n        cleared_slots: ClearedSlotsCat,\n        cleared_stage_slots: ClearedStageSlots,\n        unknown: dict[int, bool],\n    ):\n        self.cleared_slots = cleared_slots\n        self.cleared_stage_slots = cleared_stage_slots\n        self.unknown = unknown\n\n    @staticmethod\n    def init() -> ClearedSlots:\n        return ClearedSlots(\n            ClearedSlotsCat.init(),\n            ClearedStageSlots.init(),\n            {},\n        )\n\n    @staticmethod\n    def read(stream: core.Data) -> ClearedSlots:\n        cleared_slots = ClearedSlotsCat.read(stream)\n        cleared_stage_slots = ClearedStageSlots.read(stream)\n        length = stream.read_short()\n        unknown = stream.read_short_bool_dict(length)\n        return ClearedSlots(cleared_slots, cleared_stage_slots, unknown)\n\n    def write(self, stream: core.Data):\n        self.cleared_slots.write(stream)\n        self.cleared_stage_slots.write(stream)\n        stream.write_short(len(self.unknown))\n        stream.write_short_bool_dict(self.unknown, write_length=False)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"cleared_slots\": self.cleared_slots.serialize(),\n            \"cleared_stage_slots\": self.cleared_stage_slots.serialize(),\n            \"unknown\": self.unknown,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> ClearedSlots:\n        return ClearedSlots(\n            ClearedSlotsCat.deserialize(data.get(\"cleared_slots\", [])),\n            ClearedStageSlots.deserialize(data.get(\"cleared_stage_slots\", {})),\n            data.get(\"unknown\", {}),\n        )\n\n    def __repr__(self):\n        return f\"ClearedSlots({self.cleared_slots}, {self.cleared_stage_slots}, {self.unknown})\"\n\n    def __str__(self):\n        return self.__repr__()\n"
  },
  {
    "path": "src/bcsfe/core/game/battle/enemy.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\n\nclass Enemy:\n    def __init__(self, id: int):\n        self.id = id\n\n    def unlock_enemy_guide(self, save_file: core.SaveFile):\n        save_file.enemy_guide[self.id] = 1\n\n    def reset_enemy_guide(self, save_file: core.SaveFile):\n        save_file.enemy_guide[self.id] = 0\n\n    def get_name(self, save_file: core.SaveFile) -> str | None:\n        return core.core_data.get_enemy_names(save_file).get_name(self.id)\n\n\nclass EnemyDictionaryItem:\n    def __init__(self, enemy_id: int, scale: int, first_seen: int | None):\n        self.enemy_id = enemy_id\n        self.scale = scale\n        self.first_seen = first_seen\n\n\nclass EnemyDictionary:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.dictionary = self.__get_dictionary()\n\n    def __get_dictionary(self) -> list[EnemyDictionaryItem] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        csv_data = gdg.download(\"DataLocal\", \"enemy_dictionary_list.csv\")\n        if csv_data is None:\n            return None\n\n        csv = core.CSV(csv_data)\n        data: list[EnemyDictionaryItem] = []\n\n        for row in csv:\n            first_seen = None\n            if len(row) >= 3:\n                first_seen = row[2].to_int()\n            data.append(\n                EnemyDictionaryItem(row[0].to_int(), row[1].to_int(), first_seen)\n            )\n\n        return data\n\n    def get_valid_enemies(self) -> list[int] | None:\n        if self.dictionary is None:\n            return None\n\n        return [enemy.enemy_id for enemy in self.dictionary]\n\n    def get_invalid_enemies(self, total_enemies: int) -> list[int] | None:\n        valid_enemies = self.get_valid_enemies()\n        if valid_enemies is None:\n            return None\n\n        valid_enemies = set(valid_enemies)\n\n        return list(filter(lambda i: i not in valid_enemies, range(total_enemies)))\n\n\nclass EnemyDescription:\n    def __init__(self, trait_str: str, description: list[str] | None):\n        self.trait_str = trait_str\n        self.description = description\n\n\nclass EnemyDescriptions:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.descriptions = self.__get_descriptions()\n\n    def __get_descriptions(self) -> list[EnemyDescription] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\n            \"resLocal\",\n            f\"EnemyPictureBook_{core.core_data.get_lang(self.save_file)}.csv\",\n        )\n        if data is None:\n            return None\n\n        csv = core.CSV(data, core.Delimeter.from_country_code_res(self.save_file.cc))\n        descriptions: list[EnemyDescription] = []\n\n        for i, row in enumerate(csv):\n            if len(row) == 1:\n                descriptions.append(EnemyDescription(row[0].to_str(), None))\n            else:\n                descriptions.append(\n                    EnemyDescription(row[0].to_str(), row[1:].to_str_list())\n                )\n\n        return descriptions\n\n\nclass EnemyNames:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.names = self.get_names()\n\n    def get_names(self) -> list[str] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"resLocal\", \"Enemyname.tsv\")\n        if data is None:\n            return None\n        csv = core.CSV(\n            data,\n            \"\\t\",\n            remove_empty=False,\n        )\n        names: list[str] = []\n        for row in csv:\n            names.append(row[0].to_str())\n\n        return names\n\n    def get_name(self, id: int) -> str | None:\n        if self.names is None:\n            return None\n        try:\n            name = self.names[id]\n            if not name:\n                return core.core_data.local_manager.get_key(\n                    \"enemy_not_in_name_list\", id=id\n                )\n        except IndexError:\n            return core.core_data.local_manager.get_key(\"enemy_unknown_name\", id=id)\n        return name\n"
  },
  {
    "path": "src/bcsfe/core/game/battle/slots.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator\n\n\nclass EquipSlot:\n    def __init__(self, cat_id: int):\n        self.cat_id = cat_id\n\n    @staticmethod\n    def read(stream: core.Data) -> EquipSlot:\n        return EquipSlot(stream.read_int())\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.cat_id)\n\n    def serialize(self) -> int:\n        return self.cat_id\n\n    @staticmethod\n    def deserialize(data: int) -> EquipSlot:\n        return EquipSlot(data)\n\n    def __repr__(self):\n        return f\"EquipSlot({self.cat_id})\"\n\n    def __str__(self):\n        return f\"EquipSlot({self.cat_id})\"\n\n\nclass EquipSlots:\n    def __init__(self, slots: list[EquipSlot]):\n        self.slots = slots\n        self.name = \"\"\n\n    @staticmethod\n    def read(stream: core.Data) -> EquipSlots:\n        length = 10\n        slots = [EquipSlot.read(stream) for _ in range(length)]\n        return EquipSlots(slots)\n\n    @staticmethod\n    def init() -> EquipSlots:\n        length = 10\n        slots = [EquipSlot(-1) for _ in range(length)]\n        return EquipSlots(slots)\n\n    def write(self, stream: core.Data):\n        for slot in self.slots:\n            slot.write(stream)\n\n    def read_name(self, stream: core.Data):\n        length = stream.read_int()\n        try:\n            self.name = stream.read_string(length)\n        except UnicodeDecodeError:\n            stream.pos -= length\n            self.name = stream.read_utf8_string_by_char_length(length)\n\n    def write_name(self, stream: core.Data):\n        stream.write_string(self.name)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"slots\": [slot.serialize() for slot in self.slots],\n            \"name\": self.name,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> EquipSlots:\n        slots = EquipSlots(\n            [EquipSlot.deserialize(slot) for slot in data.get(\"slots\", [])]\n        )\n        slots.name = data.get(\"name\")\n        return slots\n\n    def __repr__(self):\n        return f\"EquipSlots({self.slots}, {self.name})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass LineUps:\n    def __init__(self, slots: list[EquipSlots], total_slots: int = 15):\n        self.slots = slots\n        self.selected_slot = 0\n        self.unlocked_slots = 0\n        self.slot_names_length = total_slots\n\n    @staticmethod\n    def init(gv: core.GameVersion) -> LineUps:\n        if gv < 90700:\n            length = 10\n        else:\n            length = 15\n        slots = [EquipSlots.init() for _ in range(length)]\n        return LineUps(slots, length)\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> LineUps:\n        if gv < 90700:\n            length = 10\n        else:\n            length = stream.read_byte()\n        slots = [EquipSlots.read(stream) for _ in range(length)]\n        return LineUps(slots)\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        if gv >= 90700:\n            stream.write_byte(len(self.slots))\n            length = len(self.slots)\n        else:\n            length = 10\n        if length > len(self.slots):\n            self.slots += [EquipSlots.init() for _ in range(length)]\n        else:\n            self.slots = self.slots[:length]\n        for slot in self.slots:\n            slot.write(stream)\n\n    def read_2(self, stream: core.Data, gv: core.GameVersion):\n        self.selected_slot = stream.read_int()\n        if gv < 90700:\n            unlocked_slots_l = stream.read_bool_list(10)\n            unlocked_slots = sum(unlocked_slots_l)\n        else:\n            unlocked_slots = stream.read_byte()\n        self.unlocked_slots = unlocked_slots\n\n    def write_2(self, stream: core.Data, gv: core.GameVersion):\n        stream.write_int(self.selected_slot)\n        if gv < 90700:\n            unlocked_slots_l = [False] * 10\n            unlocked_slots = min(self.unlocked_slots, 10)\n            for i in range(unlocked_slots):\n                unlocked_slots_l[i] = True\n            stream.write_bool_list(unlocked_slots_l, write_length=False)\n        else:\n            stream.write_byte(self.unlocked_slots)\n\n    def read_slot_names(self, stream: core.Data, gv: core.GameVersion):\n        if gv >= 110600:\n            total_slots = stream.read_byte()\n        else:\n            total_slots = 15\n        for i in range(total_slots):\n            try:\n                self.slots[i].read_name(stream)\n            except IndexError:\n                slot = EquipSlots.init()\n                slot.read_name(stream)\n                self.slots.append(slot)\n\n        self.slot_names_length = total_slots\n\n    def write_slot_names(self, stream: core.Data, gv: core.GameVersion):\n        if gv >= 110600:\n            stream.write_byte(self.slot_names_length)\n        for i in range(self.slot_names_length):\n            try:\n                self.slots[i].write_name(stream)\n            except IndexError:\n                slot = EquipSlots.init()\n                slot.write_name(stream)\n                self.slots.append(slot)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"slots\": [slot.serialize() for slot in self.slots],\n            \"selected_slot\": self.selected_slot,\n            \"unlocked_slots\": self.unlocked_slots,\n            \"slot_names_length\": self.slot_names_length,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> LineUps:\n        line_ups = LineUps(\n            [EquipSlots.deserialize(slot) for slot in data.get(\"slots\", [])]\n        )\n        line_ups.selected_slot = data.get(\"selected_slot\", 0)\n        line_ups.unlocked_slots = data.get(\"unlocked_slots\", 0)\n        line_ups.slot_names_length = data.get(\"slot_names_length\", 0)\n        return line_ups\n\n    def __repr__(self):\n        return f\"LineUps({self.slots}, {self.selected_slot}, {self.unlocked_slots})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def edit_unlocked_slots(self):\n        self.unlocked_slots = dialog_creator.SingleEditor(\n            \"unlocked_slots\",\n            self.unlocked_slots,\n            self.slot_names_length,\n            localized_item=True,\n            remove_alias=True,\n        ).edit()\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/__init__.py",
    "content": "from bcsfe.core.game.catbase import (\n    gatya_item,\n    stamp,\n    cat,\n    upgrade,\n    special_skill,\n    my_sale,\n    gatya,\n    user_rank_rewards,\n    item_pack,\n    login_bonuses,\n    scheme_items,\n    unlock_popups,\n    beacon_base,\n    mission,\n    nyanko_club,\n    officer_pass,\n    medals,\n    talent_orbs,\n    matatabi,\n    powerup,\n    drop_chara,\n    playtime,\n    gambling,\n)\n\n__all__ = [\n    \"stamp\",\n    \"cat\",\n    \"upgrade\",\n    \"special_skill\",\n    \"my_sale\",\n    \"gatya\",\n    \"user_rank_rewards\",\n    \"item_pack\",\n    \"login_bonuses\",\n    \"scheme_items\",\n    \"unlock_popups\",\n    \"beacon_base\",\n    \"mission\",\n    \"nyanko_club\",\n    \"officer_pass\",\n    \"medals\",\n    \"talent_orbs\",\n    \"gatya_item\",\n    \"matatabi\",\n    \"powerup\",\n    \"drop_chara\",\n    \"playtime\",\n    \"gambling\",\n]\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/beacon_base.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass BeaconEventListScene:\n    def __init__(\n        self,\n        int_dict: dict[int, int],\n        str_dict: dict[int, list[str]],\n        bool_dict: dict[int, bool],\n    ):\n        self.int_array = int_dict\n        self.str_array = str_dict\n        self.bool_array = bool_dict\n\n    @staticmethod\n    def init() -> BeaconEventListScene:\n        return BeaconEventListScene({}, {}, {})\n\n    @staticmethod\n    def read(stream: core.Data) -> BeaconEventListScene:\n        int_dict = {}\n        str_dict = {}\n        bool_dict = {}\n        for _ in range(stream.read_int()):\n            int_dict[stream.read_int()] = stream.read_int()\n        for _ in range(stream.read_int()):\n            str_dict[stream.read_int()] = stream.read_string_list()\n        for _ in range(stream.read_int()):\n            bool_dict[stream.read_int()] = stream.read_bool()\n        return BeaconEventListScene(int_dict, str_dict, bool_dict)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.int_array))\n        for key, value in self.int_array.items():\n            stream.write_int(key)\n            stream.write_int(value)\n        stream.write_int(len(self.str_array))\n        for key, value in self.str_array.items():\n            stream.write_int(key)\n            stream.write_string_list(value)\n        stream.write_int(len(self.bool_array))\n        for key, value in self.bool_array.items():\n            stream.write_int(key)\n            stream.write_bool(value)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"int_array\": self.int_array,\n            \"str_array\": self.str_array,\n            \"bool_array\": self.bool_array,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> BeaconEventListScene:\n        return BeaconEventListScene(\n            data.get(\"int_array\", []),\n            data.get(\"str_array\", []),\n            data.get(\"bool_array\", []),\n        )\n\n    def __repr__(self):\n        return f\"BeaconEventListScene({self.int_array}, {self.str_array}, {self.bool_array})\"\n\n    def __str__(self):\n        return f\"BeaconEventListScene({self.int_array}, {self.str_array}, {self.bool_array})\"\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/cat.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass SkillLevel:\n    def __init__(\n        self,\n        id: int,\n        levels: list[int],\n    ):\n        self.id = id\n        self.levels = levels\n\n    def get_total_levels(self) -> int:\n        return len(self.levels)\n\n    @staticmethod\n    def from_row(row: core.Row):\n        id = row[0].to_int()\n        levels = row[1:].to_int_list()\n        return SkillLevel(id, levels)\n\n\nclass SkillLevelData:\n    def __init__(self, levels: list[SkillLevel] | None):\n        self.levels = levels\n\n    @staticmethod\n    def from_game_data(save_file: core.SaveFile) -> SkillLevelData | None:\n        gdg = core.core_data.get_game_data_getter(save_file)\n        data = gdg.download(\"DataLocal\", \"SkillLevel.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        levels: list[SkillLevel] = []\n        for line in csv.lines[1:]:\n            levels.append(SkillLevel.from_row(line))\n        return SkillLevelData(levels)\n\n    def get_skill_level(self, id: int) -> SkillLevel | None:\n        if self.levels is None:\n            return None\n        for level in self.levels:\n            if level.id == id:\n                return level\n        return None\n\n\nclass Skill:\n    def __init__(\n        self,\n        ability_id: int,\n        max_lv: int,\n        min1: int,\n        max1: int,\n        min2: int,\n        max2: int,\n        min3: int,\n        max3: int,\n        min4: int,\n        max4: int,\n        text_id: int,\n        lvid: int,\n        name_id: int,\n        limit: int,\n    ):\n        self.ability_id = ability_id\n        self.max_lv = max_lv\n        self.min1 = min1\n        self.max1 = max1\n        self.min2 = min2\n        self.max2 = max2\n        self.min3 = min3\n        self.max3 = max3\n        self.min4 = min4\n        self.max4 = max4\n        self.text_id = text_id\n        self.lvid = lvid\n        self.name_id = name_id\n        self.limit = limit\n\n\nclass CatSkill:\n    def __init__(\n        self,\n        cat_id: int,\n        type_id: int,\n        skills: list[Skill],\n    ):\n        self.cat_id = cat_id\n        self.type_id = type_id\n        self.skills = skills\n\n    @staticmethod\n    def from_row(row: core.Row):\n        cat_id = row[0].to_int()\n        type_id = row[1].to_int()\n        skills: list[Skill] = []\n        for i in range(2, len(row), 14):\n            skill = Skill(\n                row[i].to_int(),\n                row[i + 1].to_int(),\n                row[i + 2].to_int(),\n                row[i + 3].to_int(),\n                row[i + 4].to_int(),\n                row[i + 5].to_int(),\n                row[i + 6].to_int(),\n                row[i + 7].to_int(),\n                row[i + 8].to_int(),\n                row[i + 9].to_int(),\n                row[i + 10].to_int(),\n                row[i + 11].to_int(),\n                row[i + 12].to_int(),\n                row[i + 13].to_int(),\n            )\n            skills.append(skill)\n        return CatSkill(cat_id, type_id, skills)\n\n\nclass CatSkills:\n    def __init__(self, skills: dict[int, CatSkill]):\n        self.skills = skills\n\n    @staticmethod\n    def from_game_data(save_file: core.SaveFile) -> CatSkills | None:\n        gdg = core.core_data.get_game_data_getter(save_file)\n        data = gdg.download(\"DataLocal\", \"SkillAcquisition.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        skills: dict[int, CatSkill] = {}\n        for line in csv.lines[1:]:\n            skill = CatSkill.from_row(line)\n            skills[skill.cat_id] = skill\n        return CatSkills(skills)\n\n    def get_cat_skill(self, cat_id: int) -> CatSkill | None:\n        return self.skills.get(cat_id)\n\n\nclass SkillNames:\n    def __init__(self, names: dict[int, str]):\n        self.names = names\n\n    @staticmethod\n    def from_game_data(save_file: core.SaveFile) -> SkillNames | None:\n        gdg = core.core_data.get_game_data_getter(save_file)\n        data = gdg.download(\"resLocal\", \"SkillDescriptions.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(\n            data, delimiter=core.Delimeter.from_country_code_res(save_file.cc)\n        )\n        names: dict[int, str] = {}\n        for line in csv.lines[1:]:\n            names[line[0].to_int()] = line[1].to_str()\n        return SkillNames(names)\n\n    def get_skill_name(self, skill_id: int) -> str | None:\n        return self.names.get(skill_id)\n\n\nclass TalentData:\n    def __init__(\n        self,\n        skill_names: SkillNames,\n        skill_levels: SkillLevelData,\n        cats: CatSkills,\n    ):\n        self.skill_names = skill_names\n        self.skill_levels = skill_levels\n        self.cats = cats\n\n    @staticmethod\n    def from_game_data(save_file: core.SaveFile) -> TalentData | None:\n        skill_names = SkillNames.from_game_data(save_file)\n        skill_levels = SkillLevelData.from_game_data(save_file)\n        cats = CatSkills.from_game_data(save_file)\n        if skill_names is None or skill_levels is None or cats is None:\n            return None\n\n        return TalentData(skill_names, skill_levels, cats)\n\n    def get_skill_name(self, skill_id: int) -> str | None:\n        return self.skill_names.get_skill_name(skill_id)\n\n    def get_skill_level(self, skill_id: int) -> SkillLevel | None:\n        return self.skill_levels.get_skill_level(skill_id)\n\n    def get_cat_skill(self, cat_id: int) -> CatSkill | None:\n        return self.cats.get_cat_skill(cat_id)\n\n    def get_skill_from_cat(self, cat_id: int, skill_id: int) -> Skill | None:\n        cat_skill = self.get_cat_skill(cat_id)\n        if cat_skill is None:\n            return None\n        for skill in cat_skill.skills:\n            if skill.ability_id == skill_id:\n                return skill\n        return None\n\n    def get_talent_from_cat_skill(self, cat: core.Cat, skill_id: int) -> Talent | None:\n        talents = cat.talents\n        if talents is None:\n            return None\n        for talent in talents:\n            if talent.id == skill_id:\n                return talent\n        return None\n\n    def get_cat_skill_name(self, cat_id: int, skill_id: int) -> str | None:\n        skill = self.get_skill_from_cat(cat_id, skill_id)\n        if skill is None:\n            return None\n        return self.get_skill_name(skill.text_id)\n\n    def get_cat_skill_level(self, cat_id: int, skill_id: int) -> SkillLevel | None:\n        skill = self.get_skill_from_cat(cat_id, skill_id)\n        if skill is None:\n            return None\n        return self.get_skill_level(skill.lvid)\n\n    def get_cat_talents(\n        self, cat: core.Cat\n    ) -> tuple[list[str], list[int], list[int], list[int]] | None:\n        talent_data_cat = self.get_cat_skill(cat.id)\n        if talent_data_cat is None or cat.talents is None:\n            return None\n        # save_talent_data = cat.talents\n        talent_names: list[str] = []\n        max_levels: list[int] = []\n        current_levels: list[int] = []\n        ids: list[int] = []\n        for skill in talent_data_cat.skills:\n            name = self.get_skill_name(skill.text_id)\n            talent = self.get_talent_from_cat_skill(cat, skill.ability_id)\n            if name is None or talent is None:\n                continue\n\n            max_level = skill.max_lv\n            if max_level == 0:\n                max_level = 1\n\n            max_levels.append(max_level)\n            talent_names.append(name.split(\"<br>\")[0])\n            current_levels.append(talent.level)\n            ids.append(skill.ability_id)\n\n        return talent_names, max_levels, current_levels, ids\n\n\nclass Talent:\n    def __init__(self, id: int, level: int):\n        self.id = id\n        self.level = level\n\n    @staticmethod\n    def init() -> Talent:\n        return Talent(0, 0)\n\n    def reset(self):\n        self.level = 0\n\n    @staticmethod\n    def read(stream: core.Data):\n        return Talent(stream.read_int(), stream.read_int())\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.id)\n        stream.write_int(self.level)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"id\": self.id,\n            \"level\": self.level,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Talent:\n        return Talent(\n            data[\"id\"],\n            data[\"level\"],\n        )\n\n    def __repr__(self):\n        return f\"Talent({self.id}, {self.level})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass NyankoPictureBookCatData:\n    def __init__(\n        self,\n        cat_id: int,\n        is_displayed_in_catguide: bool,\n        limited: bool,\n        total_forms: int,\n        hint_display_type: int,\n        scale_0: int,\n        scale_1: int,\n        scale_2: int,\n        scale_3: int,\n    ):\n        self.cat_id = cat_id\n        self.is_displayed_in_catguide = is_displayed_in_catguide\n        self.limited = limited\n        self.total_forms = total_forms\n        self.hint_display_type = hint_display_type\n        self.scale_0 = scale_0\n        self.scale_1 = scale_1\n        self.scale_2 = scale_2\n        self.scale_3 = scale_3\n\n\nclass NyankoPictureBook:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.cats = self.get_cats()\n\n    def get_cats(self) -> list[NyankoPictureBookCatData] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"nyankoPictureBookData.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        cats: list[NyankoPictureBookCatData] = []\n        for i, line in enumerate(csv):\n            cat = NyankoPictureBookCatData(\n                i,\n                line[0].to_bool(),\n                line[1].to_bool(),\n                line[2].to_int(),\n                line[3].to_int(),\n                line[4].to_int(),\n                line[5].to_int(),\n                line[6].to_int(),\n                line[7].to_int(),\n            )\n            cats.append(cat)\n        return cats\n\n    def get_cat(self, cat_id: int) -> NyankoPictureBookCatData | None:\n        if self.cats is None:\n            return None\n        for cat in self.cats:\n            if cat.cat_id == cat_id:\n                return cat\n        return None\n\n    def get_obtainable_cats(self) -> list[NyankoPictureBookCatData] | None:\n        if self.cats is None:\n            return None\n        return [cat for cat in self.cats if cat.is_displayed_in_catguide]\n\n\nclass EvolveItem:\n    \"\"\"Represents an item used to evolve a unit.\"\"\"\n\n    def __init__(\n        self,\n        item_id: int,\n        amount: int,\n    ):\n        \"\"\"Initializes a new EvolveItem object.\n\n        Args:\n            item_id (int): The ID of the item.\n            amount (int): The amount of the item.\n        \"\"\"\n        self.item_id = item_id\n        self.amount = amount\n\n    def __str__(self) -> str:\n        \"\"\"Gets a string representation of the EvolveItem object.\n\n        Returns:\n            str: The string representation of the EvolveItem object.\n        \"\"\"\n        return f\"{self.item_id}:{self.amount}\"\n\n    def __repr__(self) -> str:\n        \"\"\"Gets a string representation of the EvolveItem object.\n\n        Returns:\n            str: The string representation of the EvolveItem object.\n        \"\"\"\n        return str(self)\n\n\nclass EvolveItems:\n    \"\"\"Represents the items used to evolve a unit.\"\"\"\n\n    def __init__(self, evolve_items: list[EvolveItem]):\n        \"\"\"Initializes a new EvolveItems object.\n\n        Args:\n            evolve_items (list[EvolveItem]): The items used to evolve a unit.\n        \"\"\"\n        self.evolve_items = evolve_items\n\n    @staticmethod\n    def from_unit_buy_list(raw_data: core.Row, start_index: int) -> EvolveItems:\n        \"\"\"Creates a new EvolveItems object from a row from unitbuy.csv.\n\n        Args:\n            raw_data (core.Row): The row from unitbuy.csv.\n\n        Returns:\n            EvolveItems: The EvolveItems object.\n        \"\"\"\n        items: list[EvolveItem] = []\n        for i in range(5):\n            item_id = raw_data[start_index + i * 2].to_int()\n            amount = raw_data[start_index + 1 + i * 2].to_int()\n            items.append(EvolveItem(item_id, amount))\n        return EvolveItems(items)\n\n\nclass UnitBuyCatData:\n    def __init__(self, id: int, raw_data: core.Row):\n        self.id = id\n        self.assign(raw_data)\n\n    def assign(self, raw_data: core.Row):\n        self.stage_unlock = raw_data[0].to_int()\n        self.purchase_cost = raw_data[1].to_int()\n        self.upgrade_costs = [cost.to_int() for cost in raw_data[2:12]]\n        self.unlock_source = raw_data[12].to_int()\n        self.rarity = raw_data[13].to_int()\n        self.position_order = raw_data[14].to_int()\n        self.chapter_unlock = raw_data[15].to_int()\n        self.sell_price = raw_data[16].to_int()\n        self.gatya_rarity = raw_data[17].to_int()\n        self.original_max_levels = raw_data[18].to_int(), raw_data[19].to_int()\n        self.force_true_form_level = raw_data[20].to_int()\n        self.second_form_unlock_level = raw_data[21].to_int()\n        self.unknown_22 = raw_data[22].to_int()\n        self.tf_id = raw_data[23].to_int()\n        self.ff_id = raw_data[24].to_int()\n        self.evolve_level_tf = raw_data[25].to_int()\n        self.evolve_level_ff = raw_data[26].to_int()\n        self.evolve_cost_tf = raw_data[27].to_int()\n        self.evolve_items_tf = EvolveItems.from_unit_buy_list(raw_data, 28)\n        self.evolve_cost_ff = raw_data[38].to_int()\n        self.evolve_items_ff = EvolveItems.from_unit_buy_list(raw_data, 39)\n        self.max_upgrade_level_no_catseye = raw_data[49].to_int()\n        self.max_upgrade_level_catseye = raw_data[50].to_int()\n        self.max_plus_upgrade_level = raw_data[51].to_int()\n        self.unknown_52 = raw_data[52].to_int()\n        self.unknown_53 = raw_data[53].to_int()\n        self.unknown_54 = raw_data[54].to_int()\n        self.unknown_55 = raw_data[55].to_int()\n        self.catseye_usage_pattern = raw_data[56].to_int()\n        self.game_version = raw_data[57].to_int()\n        self.np_sell_price = raw_data[58].to_int()\n        self.unknwon_59 = raw_data[59].to_int()\n        self.unknown_60 = raw_data[60].to_int()\n        self.egg_value = raw_data[61].to_int()\n        self.egg_id = raw_data[62].to_int()\n\n\nclass UnitBuy:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.unit_buy = self.read_unit_buy()\n\n    def read_unit_buy(self) -> list[UnitBuyCatData] | None:\n        unit_buy: list[UnitBuyCatData] = []\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"unitbuy.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        for i, line in enumerate(csv):\n            unit_buy.append(UnitBuyCatData(i, line))\n        return unit_buy\n\n    def get_unit_buy(self, id: int) -> UnitBuyCatData | None:\n        if self.unit_buy is None:\n            return None\n        try:\n            return self.unit_buy[id]\n        except IndexError:\n            return None\n\n    def get_cat_rarity(self, id: int) -> int:\n        unit_buy = self.get_unit_buy(id)\n        if unit_buy is None:\n            return -1\n        return unit_buy.rarity\n\n\nclass UnitLimitCatData:\n    def __init__(self, cat_id: int, values: list[int]):\n        self.cat_id = cat_id\n        self.values = values\n\n\nclass UnitLimit:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.unit_limit = self.read_unit_limit()\n\n    def read_unit_limit(self) -> list[UnitLimitCatData] | None:\n        unit_limit: list[UnitLimitCatData] = []\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"unitlimit.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        for i, line in enumerate(csv):\n            unit_limit.append(UnitLimitCatData(i, line.to_int_list()))\n        return unit_limit\n\n    def get_unit_limit(self, id: int) -> UnitLimitCatData | None:\n        if self.unit_limit is None:\n            return None\n\n        try:\n            return self.unit_limit[id]\n        except IndexError:\n            return None\n\n\nclass Cat:\n    def __init__(self, id: int, unlocked: int):\n        self.id = id\n        self.unlocked = unlocked\n        self.talents: list[Talent] | None = None\n        self.upgrade: core.Upgrade = core.Upgrade.init()\n        self.current_form: int = 0\n        self.unlocked_forms: int = 0\n        self.gatya_seen: int = 0\n        self.max_upgrade_level: core.Upgrade = core.Upgrade.init()\n        self.catguide_collected: bool = False\n        self.fourth_form: int = 0\n        self.catseyes_used: int = 0\n\n        self.names: list[str] | None = None\n\n    def get_talent_from_id(self, id: int) -> Talent | None:\n        for talent in self.talents or []:\n            if talent.id == id:\n                return talent\n        return None\n\n    def unlock(self, save_file: core.SaveFile):\n        self.unlocked = 1\n        self.gatya_seen = 1\n        core.core_data.get_chara_drop(save_file).unlock_drops_from_cat_id(self.id)\n        save_file.unlock_equip_menu()\n\n    def remove(self, reset: bool = False, save_file: core.SaveFile | None = None):\n        self.unlocked = 0\n        if reset:\n            self.reset()\n            if save_file is not None:\n                save_file.cats.chara_new_flags[self.id] = 0\n                core.core_data.get_chara_drop(save_file).remove_drops_from_cat_id(\n                    self.id\n                )\n\n    def true_form(self, save_file: core.SaveFile, set_current_form: bool = True):\n        self.set_form(2, save_file, set_current_form)\n\n    def set_form(\n        self, form: int, save_file: core.SaveFile, set_current_form: bool = True\n    ):\n        if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT):\n            self.unlock(save_file)\n        self.unlocked_forms = form + 1\n        if set_current_form:\n            self.current_form = form\n\n    def set_form_true(\n        self,\n        save_file: core.SaveFile,\n        total_forms: int,\n        set_current_form: bool = True,\n        fourth_form: bool = False,\n    ):\n        if total_forms == 4 and self.unlocked_forms == 3 and fourth_form:\n            self.unlock_fourth_form(save_file, set_current_form)\n        elif total_forms >= 3:\n            self.true_form(save_file, set_current_form)\n        elif total_forms == 2:\n            self.unlocked_forms = 0\n            self.current_form = 1\n        else:\n            self.unlocked_forms = 0\n            self.current_form = 0\n\n    def remove_true_form(self):\n        self.unlocked_forms = 0\n        self.current_form = min(self.current_form, 1)\n        self.fourth_form = 0\n\n    def unlock_fourth_form(\n        self, save_file: core.SaveFile, set_current_form: bool = True\n    ):\n        if set_current_form:\n            self.current_form = 3\n        if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT):\n            self.unlock(save_file)\n        self.fourth_form = 2\n\n    def remove_fourth_form(self):\n        self.current_form = min(self.current_form, 2)\n        self.fourth_form = 0\n\n    def set_upgrade(\n        self,\n        save_file: core.SaveFile,\n        upgrade: core.Upgrade,\n        only_plus: bool = False,\n    ):\n        if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT):\n            self.unlock(save_file)\n        base = upgrade.base\n        plus = upgrade.plus\n        if base != -1 and not only_plus:\n            self.upgrade.base = upgrade.get_random_base()\n        if plus != -1:\n            self.upgrade.plus = upgrade.get_random_plus()\n\n    def upgrade_base(self, save_file: core.SaveFile):\n        if core.core_data.config.get_bool(core.ConfigKey.UNLOCK_CAT_ON_EDIT):\n            self.unlock(save_file)\n        self.upgrade.upgrade()\n\n    def reset(self):\n        self.unlocked = 0\n        self.current_form = 0\n        self.unlocked_forms = 0\n        self.gatya_seen = 0\n        self.catguide_collected = False\n        self.fourth_form = 0\n        self.catseyes_used = 0\n        self.upgrade.reset()\n        for talent in self.talents or []:\n            talent.reset()\n\n    @staticmethod\n    def init(id: int) -> Cat:\n        return Cat(id, 0)\n\n    @staticmethod\n    def read_unlocked(id: int, stream: core.Data):\n        return Cat(id, stream.read_int())\n\n    def write_unlocked(self, stream: core.Data):\n        stream.write_int(self.unlocked)\n\n    def read_upgrade(self, stream: core.Data):\n        self.upgrade = core.Upgrade.read(stream)\n\n    def write_upgrade(self, stream: core.Data):\n        self.upgrade.write(stream)\n\n    def read_current_form(self, stream: core.Data):\n        self.current_form = stream.read_int()\n\n    def write_current_form(self, stream: core.Data):\n        stream.write_int(self.current_form)\n\n    def read_unlocked_forms(self, stream: core.Data):\n        self.unlocked_forms = stream.read_int()\n\n    def write_unlocked_forms(self, stream: core.Data):\n        stream.write_int(self.unlocked_forms)\n\n    def read_gatya_seen(self, stream: core.Data):\n        self.gatya_seen = stream.read_int()\n\n    def write_gatya_seen(self, stream: core.Data):\n        stream.write_int(self.gatya_seen)\n\n    def read_max_upgrade_level(self, stream: core.Data):\n        level = core.Upgrade.read(stream)\n        self.max_upgrade_level = level\n\n    def write_max_upgrade_level(self, stream: core.Data):\n        self.max_upgrade_level.write(stream)\n\n    def read_catguide_collected(self, stream: core.Data):\n        self.catguide_collected = stream.read_bool()\n\n    def write_catguide_collected(self, stream: core.Data):\n        stream.write_bool(self.catguide_collected)\n\n    def read_fourth_form(self, stream: core.Data):\n        self.fourth_form = stream.read_int()\n\n    def write_fourth_form(self, stream: core.Data):\n        stream.write_int(self.fourth_form)\n\n    def read_catseyes_used(self, stream: core.Data):\n        self.catseyes_used = stream.read_int()\n\n    def write_catseyes_used(self, stream: core.Data):\n        stream.write_int(self.catseyes_used)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"id\": self.id,\n            \"unlocked\": self.unlocked,\n            \"upgrade\": self.upgrade.serialize(),\n            \"current_form\": self.current_form,\n            \"unlocked_forms\": self.unlocked_forms,\n            \"gatya_seen\": self.gatya_seen,\n            \"max_upgrade_level\": self.max_upgrade_level.serialize(),\n            \"catguide_collected\": self.catguide_collected,\n            \"fourth_form\": self.fourth_form,\n            \"catseyes_used\": self.catseyes_used,\n            \"talents\": (\n                [talent.serialize() for talent in self.talents]\n                if self.talents is not None\n                else None\n            ),\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Cat:\n        cat = Cat(data[\"id\"], data[\"unlocked\"])\n        cat.upgrade = core.Upgrade.deserialize(data[\"upgrade\"])\n        cat.current_form = data[\"current_form\"]\n        cat.unlocked_forms = data[\"unlocked_forms\"]\n        cat.gatya_seen = data[\"gatya_seen\"]\n        cat.max_upgrade_level = core.Upgrade.deserialize(data[\"max_upgrade_level\"])\n        cat.catguide_collected = data[\"catguide_collected\"]\n        cat.fourth_form = data[\"fourth_form\"]\n        cat.catseyes_used = data[\"catseyes_used\"]\n        cat.talents = (\n            [Talent.deserialize(talent) for talent in data[\"talents\"]]\n            if data[\"talents\"] is not None\n            else None\n        )\n        return cat\n\n    def __repr__(self) -> str:\n        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})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def read_talents(self, stream: core.Data):\n        self.talents = []\n        for _ in range(stream.read_int()):\n            self.talents.append(Talent.read(stream))\n\n    def write_talents(self, stream: core.Data):\n        if self.talents is None:\n            return\n        stream.write_int(len(self.talents))\n        for talent in self.talents:\n            talent.write(stream)\n\n    def get_names_cls(self, save_file: core.SaveFile) -> list[str] | None:\n        if self.names is None:\n            self.names = Cat.get_names(self.id, save_file)\n        return self.names\n\n    @staticmethod\n    def get_names(\n        id: int,\n        save_file: core.SaveFile,\n    ) -> list[str] | None:\n        file_name = f\"Unit_Explanation{id + 1}_{core.core_data.get_lang(save_file)}.csv\"\n        data = core.core_data.get_game_data_getter(save_file).download(\n            \"resLocal\", file_name\n        )\n        if data is None:\n            return None\n        csv = core.CSV(\n            data,\n            core.Delimeter.from_country_code_res(save_file.cc),\n            remove_empty=False,\n        )\n        names: list[str] = []\n        for line in csv.lines:\n            names.append(line[0].to_str())\n\n        return names\n\n\nclass StorageItem:\n    def __init__(self, item_id: int):\n        self.item_id = item_id\n        self.item_type = 0\n\n    @staticmethod\n    def from_cat(cat_id: int) -> StorageItem:\n        item = StorageItem(cat_id)\n        item.item_type = 1\n        return item\n\n    @staticmethod\n    def from_special_skill(special_skill_id: int) -> StorageItem:\n        item = StorageItem(special_skill_id)\n        item.item_type = 2\n        return item\n\n    @staticmethod\n    def init() -> StorageItem:\n        return StorageItem(0)\n\n    @staticmethod\n    def read_item_id(stream: core.Data):\n        return StorageItem(stream.read_int())\n\n    def write_item_id(self, stream: core.Data):\n        stream.write_int(self.item_id)\n\n    def read_item_type(self, stream: core.Data):\n        self.item_type = stream.read_int()\n\n    def write_item_type(self, stream: core.Data):\n        stream.write_int(self.item_type)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"item_id\": self.item_id,\n            \"item_type\": self.item_type,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> StorageItem:\n        item = StorageItem(data.get(\"item_id\", 0))\n        item.item_type = data.get(\"item_type\", 0)\n        return item\n\n    def __repr__(self) -> str:\n        return f\"StorageItem(item_id={self.item_id}, item_type={self.item_type})\"\n\n    def __str__(self) -> str:\n        return f\"StorageItem(item_id={self.item_id}, item_type={self.item_type})\"\n\n\nclass Cats:\n    def __init__(self, cats: list[Cat], total_storage_items: int = 0):\n        self.cats = cats\n        self.storage_items = [StorageItem.init() for _ in range(total_storage_items)]\n        self.favourites: dict[int, bool] = {}\n        self.chara_new_flags: dict[int, int] = {}\n        self.unit_buy: UnitBuy | None = None\n        self.unit_limit: UnitLimit | None = None\n        self.nyanko_picture_book: NyankoPictureBook | None = None\n        self.talent_data: TalentData | None = None\n\n    def get_all_cats(self) -> list[Cat]:\n        return self.cats\n\n    @staticmethod\n    def init(gv: core.GameVersion) -> Cats:\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            total_cats = 0\n        cats_l: list[Cat] = []\n        for i in range(total_cats):\n            cats_l.append(Cat.init(i))\n\n        if gv < 110100:\n            total_storage_items = 100\n        else:\n            total_storage_items = 0\n        return Cats(cats_l, total_storage_items)\n\n    @staticmethod\n    def get_gv_cats(gv: core.GameVersion) -> int | None:\n        if gv == 20:\n            total_cats = 203\n        elif gv == 21:\n            total_cats = 214\n        elif gv == 22:\n            total_cats = 231\n        elif gv == 23:\n            total_cats = 241\n        elif gv == 24:\n            total_cats = 249\n        elif gv == 25:\n            total_cats = 260\n        else:\n            total_cats = None\n        return total_cats\n\n    def get_unlocked_cats(self) -> list[Cat]:\n        return [cat for cat in self.cats if cat.unlocked]\n\n    def get_non_unlocked_cats(self) -> list[Cat]:\n        return [cat for cat in self.cats if not cat.unlocked]\n\n    def get_non_gacha_cats(self, save_file: core.SaveFile) -> list[Cat]:\n        unitbuy = self.read_unitbuy(save_file)\n        cats: list[Cat] = []\n        for cat in self.cats:\n            unit_buy_data = unitbuy.get_unit_buy(cat.id)\n            if unit_buy_data is None:\n                continue\n\n            if unit_buy_data.unlock_source != 2:\n                cats.append(cat)\n\n        return cats\n\n    def read_unitbuy(self, save_file: core.SaveFile) -> UnitBuy:\n        if self.unit_buy is None:\n            self.unit_buy = UnitBuy(save_file)\n        return self.unit_buy\n\n    def read_unitlimit(self, save_file: core.SaveFile) -> UnitLimit:\n        if self.unit_limit is None:\n            self.unit_limit = UnitLimit(save_file)\n        return self.unit_limit\n\n    def read_nyanko_picture_book(self, save_file: core.SaveFile) -> NyankoPictureBook:\n        if self.nyanko_picture_book is None:\n            self.nyanko_picture_book = NyankoPictureBook(save_file)\n        return self.nyanko_picture_book\n\n    def read_talent_data(self, save_file: core.SaveFile) -> TalentData | None:\n        if self.talent_data is None:\n            self.talent_data = TalentData.from_game_data(save_file)\n        return self.talent_data\n\n    def get_cats_rarity(self, save_file: core.SaveFile, rarity: int) -> list[Cat]:\n        unit_buy = self.read_unitbuy(save_file)\n        return [cat for cat in self.cats if unit_buy.get_cat_rarity(cat.id) == rarity]\n\n    def get_cats_name(\n        self,\n        save_file: core.SaveFile,\n        search_name: str,\n    ) -> list[Cat]:\n        cats: list[Cat] = []\n        for cat in self.cats:\n            names = cat.get_names_cls(save_file)\n            if names is None:\n                continue\n            for name in names:\n                if search_name.lower() in name.lower():\n                    cats.append(cat)\n                    break\n        return cats\n\n    def get_cats_obtainable(self, save_file: core.SaveFile) -> list[Cat] | None:\n        nyanko_picture_book = self.read_nyanko_picture_book(save_file)\n        obtainable_cats = nyanko_picture_book.get_obtainable_cats()\n        if obtainable_cats is None:\n            return None\n        ny_cats = [cat.cat_id for cat in obtainable_cats]\n        cats: list[Cat] = []\n        for cat in self.cats:\n            if cat.id in ny_cats:\n                cats.append(cat)\n        return cats\n\n    def get_cats_non_obtainable(self, save_file: core.SaveFile) -> list[Cat] | None:\n        nyanko_picture_book = self.read_nyanko_picture_book(save_file)\n        obtainable_cats = nyanko_picture_book.get_obtainable_cats()\n        if obtainable_cats is None:\n            return None\n        ny_cats = [cat.cat_id for cat in obtainable_cats]\n        cats: list[Cat] = []\n        for cat in self.cats:\n            if cat.id not in ny_cats:\n                cats.append(cat)\n        return cats\n\n    def get_cats_gatya_banner(\n        self, save_file: core.SaveFile, gatya_id: int\n    ) -> list[core.Cat] | None:\n        cat_ids = save_file.gatya.read_gatya_data_set(save_file).get_cat_ids(gatya_id)\n        if cat_ids is None:\n            return None\n        return self.get_cats_by_ids(cat_ids)\n\n    def true_form_cats(\n        self,\n        save_file: core.SaveFile,\n        cats: list[Cat],\n        force: bool = False,\n        set_current_forms: bool = True,\n    ):\n        pic_book = self.read_nyanko_picture_book(save_file)\n        for cat in cats:\n            pic_book_cat = pic_book.get_cat(cat.id)\n            if force:\n                cat.true_form(save_file, set_current_form=set_current_forms)\n            elif pic_book_cat is not None:\n                cat.set_form_true(\n                    save_file,\n                    pic_book_cat.total_forms,\n                    set_current_form=set_current_forms,\n                )\n\n    def fourth_form_cats(\n        self,\n        save_file: core.SaveFile,\n        cats: list[Cat],\n        force: bool = False,\n        set_current_forms: bool = True,\n    ):\n        pic_book = self.read_nyanko_picture_book(save_file)\n        for cat in cats:\n            pic_book_cat = pic_book.get_cat(cat.id)\n            if force:\n                cat.unlock_fourth_form(save_file, set_current_form=set_current_forms)\n            elif pic_book_cat is not None:\n                cat.set_form_true(\n                    save_file,\n                    pic_book_cat.total_forms,\n                    set_current_form=set_current_forms,\n                    fourth_form=True,\n                )\n\n    def get_cats_by_ids(self, ids: list[int]) -> list[Cat]:\n        cats: list[Cat] = []\n        for cat in self.cats:\n            if cat.id in ids:\n                cats.append(cat)\n        return cats\n\n    def get_cat_by_id(self, id: int) -> Cat | None:\n        for cat in self.cats:\n            if cat.id == id:\n                return cat\n        return None\n\n    @staticmethod\n    def get_rarity_names(save_file: core.SaveFile) -> list[str]:\n        localizable = save_file.get_localizable()\n        rarity_names: list[str] = []\n        rarity_index = 1\n        while True:\n            rarity_name = localizable.get(f\"rarity_name_{rarity_index}\")\n            if rarity_name is None:\n                break\n            rarity_names.append(rarity_name)\n            rarity_index += 1\n        return rarity_names\n\n    @staticmethod\n    def read_unlocked(stream: core.Data, gv: core.GameVersion) -> Cats:\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            total_cats = stream.read_int()\n        cats_l: list[Cat] = []\n        for i in range(total_cats):\n            cats_l.append(Cat.read_unlocked(i, stream))\n        return Cats(cats_l)\n\n    def write_unlocked(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_unlocked(stream)\n\n    def read_upgrade(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            total_cats = stream.read_int()\n        for cat in self.cats:\n            cat.read_upgrade(stream)\n\n    def write_upgrade(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_upgrade(stream)\n\n    def read_current_form(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            total_cats = stream.read_int()\n        for cat in self.cats:\n            cat.read_current_form(stream)\n\n    def write_current_form(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_current_form(stream)\n\n    def read_unlocked_forms(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            total_cats = stream.read_int()\n        for cat in self.cats:\n            cat.read_unlocked_forms(stream)\n\n    def write_unlocked_forms(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_unlocked_forms(stream)\n\n    def read_gatya_seen(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            total_cats = stream.read_int()\n        for cat in self.cats:\n            cat.read_gatya_seen(stream)\n\n    def write_gatya_seen(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_gatya_seen(stream)\n\n    def read_max_upgrade_levels(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            total_cats = stream.read_int()\n        for cat in self.cats:\n            cat.read_max_upgrade_level(stream)\n\n    def write_max_upgrade_levels(self, stream: core.Data, gv: core.GameVersion):\n        total_cats = Cats.get_gv_cats(gv)\n        if total_cats is None:\n            stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_max_upgrade_level(stream)\n\n    def read_storage(self, stream: core.Data, gv: core.GameVersion):\n        if gv < 110100:\n            total_storage = 100\n        else:\n            total_storage = stream.read_short()\n        self.storage_items: list[StorageItem] = []\n        for _ in range(total_storage):\n            self.storage_items.append(StorageItem.read_item_id(stream))\n        for item in self.storage_items:\n            item.read_item_type(stream)\n\n    def write_storage(self, stream: core.Data, gv: core.GameVersion):\n        if gv >= 110100:\n            stream.write_short(len(self.storage_items))\n        for item in self.storage_items:\n            item.write_item_id(stream)\n        for item in self.storage_items:\n            item.write_item_type(stream)\n\n    def read_catguide_collected(self, stream: core.Data):\n        total_cats = stream.read_int()\n        for i in range(total_cats):\n            self.cats[i].read_catguide_collected(stream)\n\n    def write_catguide_collected(self, stream: core.Data):\n        stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_catguide_collected(stream)\n\n    def read_fourth_forms(self, stream: core.Data):\n        total_cats = stream.read_int()\n        for i in range(total_cats):\n            self.cats[i].read_fourth_form(stream)\n\n    def read_catseyes_used(self, stream: core.Data):\n        total_cats = stream.read_int()\n        for i in range(total_cats):\n            self.cats[i].read_catseyes_used(stream)\n\n    def write_catseyes_used(self, stream: core.Data):\n        stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_catseyes_used(stream)\n\n    def write_fourth_forms(self, stream: core.Data):\n        stream.write_int(len(self.cats))\n        for cat in self.cats:\n            cat.write_fourth_form(stream)\n\n    def read_favorites(self, stream: core.Data):\n        self.favourites: dict[int, bool] = {}\n        total_cats = stream.read_int()\n        for _ in range(total_cats):\n            cat_id = stream.read_int()\n            self.favourites[cat_id] = stream.read_bool()\n\n    def write_favorites(self, stream: core.Data):\n        stream.write_int(len(self.favourites))\n        for cat_id, is_favourite in self.favourites.items():\n            stream.write_int(cat_id)\n            stream.write_bool(is_favourite)\n\n    def read_chara_new_flags(self, stream: core.Data):\n        self.chara_new_flags: dict[int, int] = {}\n        total_cats = stream.read_int()\n        for _ in range(total_cats):\n            cat_id = stream.read_int()\n            self.chara_new_flags[cat_id] = stream.read_int()\n\n    def write_chara_new_flags(self, stream: core.Data):\n        stream.write_int(len(self.chara_new_flags))\n        for cat_id, new_flag in self.chara_new_flags.items():\n            stream.write_int(cat_id)\n            stream.write_int(new_flag)\n\n    def read_talents(self, stream: core.Data):\n        total_cats = stream.read_int()\n        for _ in range(total_cats):\n            cat_id = stream.read_int()\n            if cat_id < 0 or cat_id >= len(self.cats):\n                cat = Cat.init(cat_id)\n                cat.read_talents(stream)\n                continue\n            self.cats[cat_id].read_talents(stream)\n\n    def write_talents(self, stream: core.Data):\n        total_talents = 0\n        for cat in self.cats:\n            total_talents += 1 if cat.talents is not None else 0\n        stream.write_int(total_talents)\n        for cat in self.cats:\n            if cat.talents is None:\n                continue\n            stream.write_int(cat.id)\n            cat.write_talents(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"cats\": [cat.serialize() for cat in self.cats],\n            \"storage_items\": [item.serialize() for item in self.storage_items],\n            \"favorites\": self.favourites,\n            \"chara_new_flags\": self.chara_new_flags,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Cats:\n        cats_l = [Cat.deserialize(cat) for cat in data.get(\"cats\", [])]\n        cats = Cats(cats_l)\n        cats.storage_items = [\n            StorageItem.deserialize(item) for item in data.get(\"storage_items\", [])\n        ]\n        cats.favourites = data.get(\"favorites\", {})\n        cats.chara_new_flags = data.get(\"chara_new_flags\", {})\n        return cats\n\n    def __repr__(self) -> str:\n        return f\"Cats(cats={self.cats}, storage_items={self.storage_items}, favourites={self.favourites}, chara_new_flags={self.chara_new_flags})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/drop_chara.py",
    "content": "from __future__ import annotations\nfrom dataclasses import dataclass\n\nfrom bcsfe import core\n\n\n@dataclass\nclass Drop:\n    stage_id: int\n    save_id: int\n    chara_id: int\n\n\nclass CharaDrop:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.drops = self.get_drops()\n\n    def get_drops(self) -> list[Drop] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"drop_chara.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        drops: list[Drop] = []\n        for line in csv.lines[1:]:\n            drops.append(\n                Drop(\n                    stage_id=line[0].to_int(),\n                    save_id=line[1].to_int(),\n                    chara_id=line[2].to_int(),\n                )\n            )\n\n        return drops\n\n    def get_drop(self, stage_id: int) -> Drop | None:\n        if self.drops is None:\n            return None\n        for drop in self.drops:\n            if drop.stage_id == stage_id:\n                return drop\n\n        return None\n\n    def get_drops_from_chara_id(self, chara_id: int) -> list[Drop] | None:\n        if self.drops is None:\n            return None\n        drops: list[Drop] = []\n        for drop in self.drops:\n            if drop.chara_id == chara_id:\n                drops.append(drop)\n\n        return drops\n\n    def unlock_drops_from_cat_id(self, cat_id: int) -> None:\n        drops = self.get_drops_from_chara_id(cat_id)\n        if drops is None:\n            return\n        for drop in drops:\n            try:\n                self.save_file.unit_drops[drop.save_id] = 1\n            except IndexError:\n                pass\n\n    def remove_drops_from_cat_id(self, cat_id: int) -> None:\n        drops = self.get_drops_from_chara_id(cat_id)\n        if drops is None:\n            return\n        for drop in drops:\n            try:\n                self.save_file.unit_drops[drop.save_id] = 0\n            except IndexError:\n                pass\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/gambling.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom typing import Any\n\nfrom bcsfe.cli import color\n\n\nclass GamblingEvent:\n    def __init__(\n        self,\n        completed: dict[int, bool],\n        values: dict[int, dict[int, int]],\n        start_times: dict[int, int | float],\n    ):\n        self.completed = completed\n        self.values = values\n        self.start_times = start_times\n\n    @staticmethod\n    def init() -> GamblingEvent:\n        return GamblingEvent({}, {}, {})\n\n    @staticmethod\n    def read(data: core.Data, game_version: core.GameVersion) -> GamblingEvent:\n        total = data.read_short()\n        completed: dict[int, bool] = {}\n\n        for _ in range(total):\n            key = data.read_short()\n            completed[key] = data.read_bool()\n\n        total = data.read_short()\n        values: dict[int, dict[int, int]] = {}\n\n        for _ in range(total):\n            key = data.read_short()\n            if key not in values:\n                values[key] = {}\n\n            total2 = data.read_short()\n            for _ in range(total2):\n                key2 = data.read_short()\n\n                values[key][key2] = data.read_short()\n\n        total = data.read_short()\n        start_times: dict[int, int | float] = {}\n\n        for _ in range(total):\n            key = data.read_short()\n\n            if game_version < 90100:\n                value = data.read_double()\n            else:\n                value = data.read_int()\n\n            start_times[key] = value\n\n        return GamblingEvent(completed, values, start_times)\n\n    def write(self, data: core.Data, game_version: core.GameVersion):\n        data.write_short(len(self.completed))\n        data.write_short_bool_dict(self.completed, write_length=False)\n\n        data.write_short(len(self.values))\n\n        for key, value in self.values.items():\n            data.write_short(key)\n            data.write_short(len(value))\n\n            for key2, value2 in value.items():\n                data.write_short(key2)\n                data.write_short(value2)\n\n        data.write_short(len(self.start_times))\n        for key, value in self.start_times.items():\n            data.write_short(key)\n\n            # this is a bad conversion, since float is timestamp i assume and int as the date as YYYMMDD. FIXME\n            if game_version < 90100:\n                data.write_double(float(value))\n            else:\n                data.write_int(int(value))\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"completed\": self.completed,\n            \"values\": self.values,\n            \"start_times\": self.start_times,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> GamblingEvent:\n        return GamblingEvent(\n            data.get(\"completed\", {}),\n            data.get(\"values\", {}),\n            data.get(\"start_times\", {}),\n        )\n\n    def reset(self):\n        self.completed = {}\n        self.values = {}\n        # TODO: check start times\n        self.start_times = {}\n\n    @staticmethod\n    def reset_events(save_file: core.SaveFile):\n        save_file.wildcat_slots.reset()\n        color.ColoredText.localize(\"reset_wildcat_slots\")\n        save_file.cat_scratcher.reset()\n        color.ColoredText.localize(\"reset_cat_scratcher\")\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/gatya.py",
    "content": "from __future__ import annotations\nimport enum\nfrom typing import Any, Callable\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass Gatya:\n    def __init__(self, rare_seed: int, normal_seed: int):\n        self.rare_seed = rare_seed\n        self.normal_seed = normal_seed\n        self.event_seed = 0\n        self.stepup_stage_3_cooldown = 0\n        self.previous_normal_roll = 0\n        self.previous_normal_roll_type = 0\n        self.previous_rare_roll = 0\n        self.previous_rare_roll_type = 0\n        self.unknown1 = False\n        self.roll_single = False\n        self.roll_multi = False\n        self.trade_progress = 0\n        self.step_up_stages: dict[int, int] = {}\n        self.stepup_durations: dict[int, float] = {}\n\n        self.gatya_data_set: GatyaDataSet | None = None\n\n    @staticmethod\n    def init() -> Gatya:\n        return Gatya(0, 0)\n\n    @staticmethod\n    def read_rare_normal_seed(data: core.Data, gv: core.GameVersion) -> Gatya:\n        if gv < 33:\n            return Gatya(data.read_ulong(), data.read_ulong())\n        return Gatya(data.read_uint(), data.read_uint())\n\n    def read_event_seed(self, data: core.Data, gv: core.GameVersion):\n        if gv < 33:\n            self.event_seed = data.read_ulong()\n        else:\n            self.event_seed = data.read_uint()\n\n    def write_rare_normal_seed(self, data: core.Data):\n        data.write_uint(self.rare_seed)\n        data.write_uint(self.normal_seed)\n\n    def write_event_seed(self, data: core.Data):\n        data.write_uint(self.event_seed)\n\n    def read2(self, data: core.Data):\n        self.stepup_stage_3_cooldown = data.read_int()\n        self.previous_normal_roll = data.read_int()\n        self.previous_normal_roll_type = data.read_int()\n        self.previous_rare_roll = data.read_int()\n        self.previous_rare_roll_type = data.read_int()\n        self.unknown1 = data.read_bool()\n        self.roll_single = data.read_bool()\n        self.roll_multi = data.read_bool()\n\n    def write2(self, data: core.Data):\n        data.write_int(self.stepup_stage_3_cooldown)\n        data.write_int(self.previous_normal_roll)\n        data.write_int(self.previous_normal_roll_type)\n        data.write_int(self.previous_rare_roll)\n        data.write_int(self.previous_rare_roll_type)\n        data.write_bool(self.unknown1)\n        data.write_bool(self.roll_single)\n        data.write_bool(self.roll_multi)\n\n    def read_trade_progress(self, data: core.Data):\n        self.trade_progress = data.read_int()\n\n    def write_trade_progress(self, data: core.Data):\n        data.write_int(self.trade_progress)\n\n    def read_stepup(self, data: core.Data):\n        self.step_up_stages: dict[int, int] = {}\n        total = data.read_int()\n        for _ in range(total):\n            key = data.read_int()\n            self.step_up_stages[key] = data.read_int()\n\n        self.stepup_durations: dict[int, float] = {}\n        total = data.read_int()\n        for _ in range(total):\n            key = data.read_int()\n            self.stepup_durations[key] = data.read_double()\n\n    def write_stepup(self, data: core.Data):\n        data.write_int(len(self.step_up_stages))\n        for id, stage in self.step_up_stages.items():\n            data.write_int(id)\n            data.write_int(stage)\n\n        data.write_int(len(self.stepup_durations))\n        for id, duration in self.stepup_durations.items():\n            data.write_int(id)\n            data.write_double(duration)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"rare_seed\": self.rare_seed,\n            \"normal_seed\": self.normal_seed,\n            \"stepup_stage_3_cooldown\": self.stepup_stage_3_cooldown,\n            \"previous_normal_roll\": self.previous_normal_roll,\n            \"previous_normal_roll_type\": self.previous_normal_roll_type,\n            \"previous_rare_roll\": self.previous_rare_roll,\n            \"previous_rare_roll_type\": self.previous_rare_roll_type,\n            \"unknown1\": self.unknown1,\n            \"roll_single\": self.roll_single,\n            \"roll_multi\": self.roll_multi,\n            \"trade_progress\": self.trade_progress,\n            \"event_seed\": self.event_seed,\n            \"step_up_stages\": self.step_up_stages,\n            \"stepup_durations\": self.stepup_durations,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Gatya:\n        gatya = Gatya(data.get(\"rare_seed\", 0), data.get(\"normal_seed\", 0))\n        gatya.stepup_stage_3_cooldown = data.get(\"stepup_stage_3_cooldown\", 0)\n        gatya.previous_normal_roll = data.get(\"previous_normal_roll\", 0)\n        gatya.previous_normal_roll_type = data.get(\"previous_normal_roll_type\", 0)\n        gatya.previous_rare_roll = data.get(\"previous_rare_roll\", 0)\n        gatya.previous_rare_roll_type = data.get(\"previous_rare_roll_type\", 0)\n        gatya.unknown1 = data.get(\"unknown1\", False)\n        gatya.roll_single = data.get(\"roll_single\", False)\n        gatya.roll_multi = data.get(\"roll_multi\", False)\n        gatya.trade_progress = data.get(\"trade_progress\", 0)\n        gatya.event_seed = data.get(\"event_seed\", 0)\n        gatya.step_up_stages = data.get(\"step_up_stages\", {})\n        gatya.stepup_durations = data.get(\"stepup_durations\", {})\n        return gatya\n\n    def __repr__(self) -> str:\n        return f\"Gatya({self.serialize()})\"\n\n    def __str__(self) -> str:\n        return f\"Gatya({self.serialize()})\"\n\n    def edit_rare_gatya_seed(self):\n        self.rare_seed = dialog_creator.SingleEditor(\n            \"rare_gatya_seed\",\n            self.rare_seed,\n            None,\n            localized_item=True,\n            signed=False,\n        ).edit()\n\n    def edit_normal_gatya_seed(self):\n        self.normal_seed = dialog_creator.SingleEditor(\n            \"normal_gatya_seed\",\n            self.normal_seed,\n            None,\n            localized_item=True,\n            signed=False,\n        ).edit()\n\n    def edit_event_gatya_seed(self):\n        self.event_seed = dialog_creator.SingleEditor(\n            \"event_gatya_seed\",\n            self.event_seed,\n            None,\n            localized_item=True,\n            signed=False,\n        ).edit()\n\n    def read_gatya_data_set(self, save_file: core.SaveFile) -> GatyaDataSet:\n        if self.gatya_data_set is not None:\n            return self.gatya_data_set\n        self.gatya_data_set = GatyaDataSet(save_file)\n        return self.gatya_data_set\n\n\nclass GatyaDataSet:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.gatya_data_set = self.load_gatya_data_set(\"R\", 1)\n\n    def load_gatya_data_set(self, rarity: str, id: int) -> list[list[int]] | None:\n        file_name = f\"GatyaDataSet{rarity.upper()[0]}{id}.csv\"\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", file_name)\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        dt: list[list[int]] = []\n        for line in csv:\n            cat_ids: list[int] = []\n            for cat_id in line:\n                cat_id = cat_id.to_int()\n                if cat_id != -1:\n                    cat_ids.append(cat_id)\n            dt.append(cat_ids)\n        return dt\n\n    def get_cat_ids(self, gatya_id: int) -> list[int] | None:\n        if self.gatya_data_set is None:\n            return None\n        try:\n            return self.gatya_data_set[gatya_id]\n        except IndexError:\n            return None\n\n\nclass GatyaInfo:\n    def __init__(self, gatya_id: int, cc: core.CountryCode, type_str: str = \"R\"):\n        self.gatya_id = gatya_id\n        self.cc = cc\n        self.gatya_data_set: GatyaDataSet | None = None\n        self.type = type_str\n        self.data: core.Data | None = None\n\n    def get_id_str(self) -> str:\n        return f\"{self.gatya_id:03}\"\n\n    def get_cc_str(self) -> str:\n        if self.cc == core.CountryCode(\"jp\"):\n            return \"\"\n        return self.cc.get_patching_code() + \"/\"\n\n    def get_url(self) -> str:\n        return f\"https://ponosgames.com/information/appli/battlecats/gacha/rare/{self.get_cc_str()}{self.type}{self.get_id_str()}.html\"\n\n    def download_data(self) -> core.Data | None:\n        url = self.get_url()\n\n        response = core.RequestHandler(url).get()\n        if response is None:\n            return\n        data = core.Data(response.content)\n\n        self.save_data(data)\n        return data\n\n    def get_file_path(self) -> core.Path:\n        return (\n            core.Path.get_data_folder()\n            .add(\"other_game_data\")\n            .add(self.cc.get_code())\n            .add(\"gatya_info\")\n            .generate_dirs()\n            .add(f\"{self.type}{self.get_id_str()}.html\")\n        )\n\n    def save_data(self, data: core.Data):\n        try:\n            data.to_file(self.get_file_path())\n        except Exception as e:\n            color.ColoredText.localize(\"save_gatya_error\", error=e)\n        self.data = data\n\n    def load_data_from_file(self) -> core.Data | None:\n        if not self.get_file_path().exists():\n            return None\n        return core.Data.from_file(self.get_file_path())\n\n    def get_data(self) -> core.Data | None:\n        if self.data is not None:\n            return self.data\n        data = self.load_data_from_file()\n        if data is None:\n            data = self.download_data()\n        return data\n\n    def get_name(self) -> str | None:\n        data = self.get_data()\n        if data is None:\n            return None\n        # find <h2>...</h2>\n        data = data.get_bytes()\n        h2 = data.find(b\"<h2>\")\n        if h2 == -1:\n            return None\n        h2_end = data.find(b\"</h2>\", h2)\n        if h2_end == -1:\n            return None\n        text = data[h2 + 4 : h2_end].decode(\"utf-8\")\n        # remove <span...</span>\n        span = text.find(\"<span\")\n        if span == -1:\n            return text\n        span_end = text.find(\"</span>\", span)\n        if span_end == -1:\n            return text\n        return text[:span] + text[span_end + 7 :]\n\n\nclass GatyaInfos:\n    def __init__(self, save_file: core.SaveFile, type_str: str = \"R\", set_id: int = 1):\n        self.save_file = save_file\n        self.type = type_str\n        self.set_id = set_id\n        self.gatya_data_set = GatyaDataSet(save_file).load_gatya_data_set(\n            type_str, set_id\n        )\n        self.infos: list[GatyaInfo] = []\n        self.got_all = False\n\n    def get_all(\n        self,\n        threaded: bool = True,\n        print_progress: bool = True,\n        max_threads: int = 16,\n    ):\n        if self.gatya_data_set is None:\n            return\n        all_ids = len(self.gatya_data_set)\n        if threaded:\n            funcs: list[Callable[..., Any]] = []\n            args: list[list[Any]] = []\n            for id in range(all_ids):\n                funcs.append(self.get)\n                args.append([id, print_progress])\n            core.thread_run_many(funcs, args, max_threads=max_threads)\n\n        else:\n            for id in range(all_ids):\n                self.infos.append(self.get(id, print_progress=print_progress))\n\n        self.got_all = True\n\n    def get(self, gatya_id: int, print_progress: bool):\n        if print_progress:\n            color.ColoredText.localize(\n                \"gatya_info_progress\",\n                current=len(self.infos or []) + 1,\n                total=len(self.gatya_data_set or []),\n            )\n        info = GatyaInfo(gatya_id, self.save_file.cc, self.type)\n        info.get_data()\n        self.infos.append(info)\n        return info\n\n    def get_info(self, gatya_id: int) -> GatyaInfo | None:\n        if self.infos:\n            return self.infos[gatya_id]\n        return None\n\n    def get_all_names(self) -> dict[int, str]:\n        if not self.got_all:\n            self.get_all(True, max_threads=64)\n        names: dict[int, str] = {}\n        for info in self.infos:\n            names[\n                info.gatya_id\n            ] = info.get_name() or core.core_data.local_manager.get_key(\n                \"unknown_banner\"\n            )\n\n        return names\n\n\nclass GatyaDataOptionSet:\n    def __init__(\n        self,\n        id: int,\n        banner_on: bool,\n        ticket_item_id: int,\n        anim_id: int,\n        button_cut_id: int,\n        series_id: int,\n        menu_cut_id: int,\n        char_id: int | None,\n        wait_maanim: bool | None,\n    ):\n        self.id = id\n        self.banner_on = banner_on\n        self.ticket_item_id = ticket_item_id\n        self.anim_id = anim_id\n        self.button_cut_id = button_cut_id\n        self.series_id = series_id\n        self.menu_cut_id = menu_cut_id\n        self.char_id = char_id\n        self.wait_maanim = wait_maanim\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> GatyaDataOptionSet:\n        return GatyaDataOptionSet(\n            row.next_int(),\n            row.next_bool(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int_opt(),\n            row.next_bool_opt(),\n        )\n\n\nclass GatyaEventType(enum.Enum):\n    NORMAL = \"N\"\n    RARE = \"R\"\n    EVENT = \"E\"\n\n\nclass GatyaDataOption:\n    def __init__(self, sets: list[GatyaDataOptionSet]):\n        self.sets = sets\n\n    def get(self, set_id: int) -> GatyaDataOptionSet | None:\n        for gset in self.sets:\n            if gset.id == set_id:\n                return gset\n\n        return None\n\n    @staticmethod\n    def from_csv(csv: core.CSV) -> GatyaDataOption:\n        sets: list[GatyaDataOptionSet] = []\n        csv.read_line()  # skip headers\n        for row in csv:\n            sets.append(GatyaDataOptionSet.from_csv_row(row))\n\n        return GatyaDataOption(sets)\n\n    @staticmethod\n    def from_data(data: core.Data) -> GatyaDataOption:\n        return GatyaDataOption.from_csv(core.CSV(data, \"\\t\"))\n\n    @staticmethod\n    def get_filename(event_type: GatyaEventType) -> str:\n        return f\"GatyaData_Option_Set{event_type.value}.tsv\"\n\n    @staticmethod\n    def read(\n        save_file: core.SaveFile, e_type: GatyaEventType\n    ) -> GatyaDataOption | None:\n        gdg = core.core_data.get_game_data_getter(save_file)\n\n        data = gdg.download(\"DataLocal\", GatyaDataOption.get_filename(e_type))\n        if data is None:\n            return None\n\n        return GatyaDataOption.from_data(data)\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/gatya_item.py",
    "content": "from __future__ import annotations\nimport enum\nfrom bcsfe import core\n\n\nclass GatyaItemNames:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.names = self.__get_names()\n\n    def __get_names(self) -> list[str] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"resLocal\", \"GatyaitemName.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(\n            data, core.Delimeter.from_country_code_res(self.save_file.cc)\n        )\n        names: list[str] = []\n        for line in csv:\n            names.append(line[0].to_str())\n\n        return names\n\n    def get_name(self, index: int) -> str | None:\n        if self.names is None:\n            return None\n        try:\n            return self.names[index]\n        except IndexError:\n            return core.core_data.local_manager.get_key(\n                \"gatya_item_unknown_name\", index=index\n            )\n\n\nclass GatyaItemBuyItem:\n    def __init__(\n        self,\n        id: int,\n        rarity: int,\n        reflect_or_storage: bool,\n        price: int,\n        stage_drop_id: int,\n        quantity: int,\n        server_id: int,\n        category: int,\n        index: int,\n        src_item_id: int,\n        main_menu_type: int,\n        gatya_ticket_id: int,\n        comment: str,\n    ):\n        self.id = id\n        self.rarity = rarity\n        self.reflect_or_storage = reflect_or_storage\n        self.price = price\n        self.stage_drop_id = stage_drop_id\n        self.quantity = quantity\n        self.server_id = server_id\n        self.category = category\n        self.index = index\n        self.src_item_id = src_item_id\n        self.main_menu_type = main_menu_type\n        self.gatya_ticket_id = gatya_ticket_id\n        self.comment = comment\n\nclass GatyaItemCategory(enum.Enum):\n    MISC = 0\n    EVENT_TICKETS = 1\n    SPECIAL_SKILLS = 2\n    BATTLE_ITEMS = 3\n    EVOLVE_ITEMS = 4\n    CATSEYES = 5\n    CATAMINS = 6\n    BASE_MATERIALS = 7\n    LUCKY_TICKETS_1 = 8\n    ENDLESS_ITEMS = 9\n    LUCKY_TICKETS_2 = 10\n    LABYRINTH_MEDALS = 11\n    TREASURE_CHESTS = 12\n\nclass GatyaItemBuy:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.buy = self.get_buy()\n\n    def get_buy(self) -> list[GatyaItemBuyItem] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"Gatyaitembuy.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        buy: list[GatyaItemBuyItem] = []\n        for i, line in enumerate(csv.lines[1:]):\n            try:\n                buy.append(\n                    GatyaItemBuyItem(\n                        i,\n                        line[0].to_int(),\n                        line[1].to_bool(),\n                        line[2].to_int(),\n                        line[3].to_int(),\n                        line[4].to_int(),\n                        line[5].to_int(),\n                        line[6].to_int(),\n                        line[7].to_int(),\n                        line[8].to_int(),\n                        line[9].to_int(),\n                        line[10].to_int(),\n                        line[11].to_str(),\n                    )\n                )\n            except IndexError:\n                pass\n\n        return buy\n\n    def sort_by_index(self, items: list[GatyaItemBuyItem]):\n        items.sort(key=lambda x: x.index)\n        return items\n\n    def get_by_category(self, category: int | GatyaItemCategory) -> list[GatyaItemBuyItem] | None:\n        if self.buy is None:\n            return None\n        if isinstance(category, GatyaItemCategory):\n            category = category.value\n        return self.sort_by_index(\n            [item for item in self.buy if item.category == category]\n        )\n\n    def get_names_by_category(self, category: int | GatyaItemCategory) -> list[tuple[GatyaItemBuyItem, str | None]] | None:\n        items = self.get_by_category(category)\n        if items is None:\n            return None\n\n        names = GatyaItemNames(self.save_file)\n\n        return [(item, names.get_name(item.id)) for item in items]\n\n    def get(self, item_id: int) -> GatyaItemBuyItem | None:\n        if self.buy is None:\n            return None\n        if item_id < 0 or item_id >= len(self.buy):\n            return None\n\n        return self.buy[item_id]\n\n    def get_by_server_id(self, server_id: int) -> GatyaItemBuyItem | None:\n        if self.buy is None:\n            return None\n        for item in self.buy:\n            if item.server_id == server_id:\n                return item\n\n        return None\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/item_pack.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass PurchasedPack:\n    def __init__(self, purchased: bool):\n        self.purchased = purchased\n\n    @staticmethod\n    def init() -> PurchasedPack:\n        return PurchasedPack(False)\n\n    @staticmethod\n    def read(stream: core.Data) -> PurchasedPack:\n        purchased = stream.read_bool()\n        return PurchasedPack(purchased)\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.purchased)\n\n    def serialize(self) -> bool:\n        return self.purchased\n\n    @staticmethod\n    def deserialize(data: bool) -> PurchasedPack:\n        return PurchasedPack(data)\n\n    def __repr__(self) -> str:\n        return f\"PurchasedPack(purchased={self.purchased!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass PurchaseSet:\n    def __init__(self, purchases: dict[str, PurchasedPack]):\n        self.purchases = purchases\n\n    @staticmethod\n    def init() -> PurchaseSet:\n        return PurchaseSet({})\n\n    @staticmethod\n    def read(stream: core.Data) -> PurchaseSet:\n        total = stream.read_int()\n        purchases: dict[str, PurchasedPack] = {}\n        for _ in range(total):\n            key = stream.read_string()\n            purchases[key] = PurchasedPack.read(stream)\n        return PurchaseSet(purchases)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.purchases))\n        for key, purchase in self.purchases.items():\n            stream.write_string(key)\n            purchase.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            key: purchase.serialize()\n            for key, purchase in self.purchases.items()\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> PurchaseSet:\n        return PurchaseSet(\n            {\n                key: PurchasedPack.deserialize(purchase)\n                for key, purchase in data.items()\n            },\n        )\n\n    def __repr__(self) -> str:\n        return f\"PurchaseSet(purchases={self.purchases!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass Purchases:\n    def __init__(self, purchases: dict[int, PurchaseSet]):\n        self.purchases = purchases\n\n    @staticmethod\n    def init() -> Purchases:\n        return Purchases({})\n\n    @staticmethod\n    def read(stream: core.Data) -> Purchases:\n        total = stream.read_int()\n        purchases: dict[int, PurchaseSet] = {}\n        for _ in range(total):\n            key = stream.read_int()\n            purchases[key] = PurchaseSet.read(stream)\n\n        return Purchases(purchases)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.purchases))\n        for key, purchase in self.purchases.items():\n            stream.write_int(key)\n            purchase.write(stream)\n\n    def serialize(self) -> dict[int, Any]:\n        return {\n            key: purchase.serialize()\n            for key, purchase in self.purchases.items()\n        }\n\n    @staticmethod\n    def deserialize(data: dict[int, Any]) -> Purchases:\n        return Purchases(\n            {\n                key: PurchaseSet.deserialize(purchase)\n                for key, purchase in data.items()\n            },\n        )\n\n    def __repr__(self) -> str:\n        return f\"Purchases(purchases={self.purchases!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass ItemPack:\n    def __init__(self, purchases: Purchases):\n        self.purchases = purchases\n        self.displayed_packs: dict[int, bool] = {}\n        self.three_days_started: bool = False\n        self.three_days_end_timestamp: float = 0.0\n\n    @staticmethod\n    def init() -> ItemPack:\n        return ItemPack(Purchases.init())\n\n    @staticmethod\n    def read(stream: core.Data) -> ItemPack:\n        return ItemPack(Purchases.read(stream))\n\n    def write(self, stream: core.Data):\n        self.purchases.write(stream)\n\n    def read_displayed_packs(self, stream: core.Data) -> None:\n        total = stream.read_int()\n        displayed_packs: dict[int, bool] = {}\n        for _ in range(total):\n            key = stream.read_int()\n            displayed_packs[key] = stream.read_bool()\n\n        self.displayed_packs = displayed_packs\n\n    def write_displayed_packs(self, stream: core.Data) -> None:\n        stream.write_int(len(self.displayed_packs))\n        for key, displayed in self.displayed_packs.items():\n            stream.write_int(key)\n            stream.write_bool(displayed)\n\n    def read_three_days(self, stream: core.Data) -> None:\n        self.three_days_started = stream.read_bool()\n        self.three_days_end_timestamp = stream.read_double()\n\n    def write_three_days(self, stream: core.Data) -> None:\n        stream.write_bool(self.three_days_started)\n        stream.write_double(self.three_days_end_timestamp)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"purchases\": self.purchases.serialize(),\n            \"displayed_packs\": self.displayed_packs,\n            \"three_days_started\": self.three_days_started,\n            \"three_days_end_timestamp\": self.three_days_end_timestamp,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> ItemPack:\n        item_pack = ItemPack(Purchases.deserialize(data.get(\"purchases\", {})))\n        item_pack.displayed_packs = data.get(\"displayed_packs\", {})\n        item_pack.three_days_started = data.get(\"three_days_started\", False)\n        item_pack.three_days_end_timestamp = data.get(\n            \"three_days_end_timestamp\", 0.0\n        )\n        return item_pack\n\n    def __repr__(self) -> str:\n        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})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/login_bonuses.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass Login:\n    def __init__(self, count: int):\n        self.count = count\n\n    @staticmethod\n    def init() -> Login:\n        return Login(0)\n\n    @staticmethod\n    def read(stream: core.Data) -> Login:\n        count = stream.read_int()\n        return Login(count)\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.count)\n\n    def serialize(self) -> int:\n        return self.count\n\n    @staticmethod\n    def deserialize(data: int) -> Login:\n        return Login(data)\n\n    def __repr__(self):\n        return f\"Login({self.count})\"\n\n    def __str__(self):\n        return f\"Login({self.count})\"\n\n\nclass Logins:\n    def __init__(self, logins: list[Login]):\n        self.logins = logins\n\n    @staticmethod\n    def init() -> Logins:\n        return Logins([])\n\n    @staticmethod\n    def read(stream: core.Data) -> Logins:\n        total = stream.read_int()\n        logins: list[Login] = []\n        for _ in range(total):\n            logins.append(Login.read(stream))\n        return Logins(logins)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.logins))\n        for login in self.logins:\n            login.write(stream)\n\n    def serialize(self) -> list[int]:\n        return [login.serialize() for login in self.logins]\n\n    @staticmethod\n    def deserialize(data: list[int]) -> Logins:\n        return Logins([Login.deserialize(login) for login in data])\n\n    def __repr__(self):\n        return f\"Logins({self.logins})\"\n\n    def __str__(self):\n        return f\"Logins({self.logins})\"\n\n\nclass LoginSets:\n    def __init__(self, logins: list[Logins]):\n        self.logins = logins\n\n    @staticmethod\n    def init() -> LoginSets:\n        return LoginSets([])\n\n    @staticmethod\n    def read(stream: core.Data) -> LoginSets:\n        total = stream.read_int()\n        logins: list[Logins] = []\n        for _ in range(total):\n            logins.append(Logins.read(stream))\n        return LoginSets(logins)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.logins))\n        for login in self.logins:\n            login.write(stream)\n\n    def serialize(self) -> list[list[int]]:\n        return [login.serialize() for login in self.logins]\n\n    @staticmethod\n    def deserialize(data: list[list[int]]) -> LoginSets:\n        return LoginSets([Logins.deserialize(login) for login in data])\n\n    def __repr__(self):\n        return f\"LoginSets({self.logins})\"\n\n    def __str__(self):\n        return f\"LoginSets({self.logins})\"\n\n\nclass LoginBonus:\n    def __init__(\n        self,\n        old_logins: LoginSets | None = None,\n        logins: dict[int, Login] | None = None,\n    ):\n        self.old_logins = old_logins\n        self.logins = logins\n\n    @staticmethod\n    def init(gv: core.GameVersion) -> LoginBonus:\n        if gv < 80000:\n            return LoginBonus(old_logins=LoginSets.init())\n        else:\n            return LoginBonus(logins={})\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> LoginBonus:\n        if gv < 80000:\n            logins_old = LoginSets.read(stream)\n            return LoginBonus(logins_old)\n        else:\n            total = stream.read_int()\n            logins: dict[int, Login] = {}\n            for _ in range(total):\n                id = stream.read_int()\n                logins[id] = Login.read(stream)\n            return LoginBonus(logins=logins)\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        if gv < 80000:\n            (self.old_logins or LoginSets([])).write(stream)\n        elif gv >= 80000:\n            logins = self.logins or {}\n            stream.write_int(len(logins))\n            for id, login in logins.items():\n                stream.write_int(id)\n                login.write(stream)\n\n    def serialize(\n        self,\n    ) -> dict[str, Any]:\n        if self.old_logins is not None:\n            return {\"old_logins\": self.old_logins.serialize()}\n        elif self.logins is not None:\n            return {\n                \"logins\": {\n                    id: login.serialize() for id, login in self.logins.items()\n                }\n            }\n        else:\n            return {}\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> LoginBonus:\n        if \"old_logins\" in data:\n            return LoginBonus(\n                old_logins=LoginSets.deserialize(data[\"old_logins\"])\n            )\n        elif \"logins\" in data:\n            return LoginBonus(\n                logins={\n                    int(id): Login.deserialize(login)\n                    for id, login in data[\"logins\"].items()\n                }\n            )\n        else:\n            return LoginBonus()\n\n    def __repr__(self):\n        return f\"LoginBonus({self.old_logins}, {self.logins})\"\n\n    def __str__(self):\n        return f\"LoginBonus({self.old_logins}, {self.logins})\"\n\n    def get_login(self, id: int) -> Login | None:\n        if self.logins is not None:\n            return self.logins.get(id)\n        else:\n            return None\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/matatabi.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\n\nclass Fruit:\n    def __init__(\n        self,\n        id: int,\n        seed: bool,\n        group: int,\n        sort: int,\n        require: int | None = None,\n        text: str | None = None,\n        grow_up: list[int] | None = None,\n    ):\n        self.id = id\n        self.seed = seed\n        self.group = group\n        self.sort = sort\n        self.require = require\n        self.text = text\n        self.grow_up = grow_up\n\n\nclass Matatabi:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.matatabi = self.__get_matatabi()\n        self.gatya_item_names = core.core_data.get_gatya_item_names(\n            self.save_file\n        )\n\n    def __get_matatabi(self) -> list[Fruit] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"Matatabi.tsv\")\n        if data is None:\n            return None\n        csv = core.CSV(data, \"\\t\")\n        matatabi: list[Fruit] = []\n        for line in csv.lines[1:]:\n            id = line[0].to_int()\n            seed = line[1].to_bool()\n            group = line[2].to_int()\n            sort = line[3].to_int()\n            if len(line) > 4:\n                require = line[4].to_int()\n            else:\n                require = None\n            if len(line) > 5:\n                text = line[5].to_str()\n            else:\n                text = None\n            if len(line) > 6:\n                grow_up = [item.to_int() for item in line[6:]]\n            else:\n                grow_up = None\n            matatabi.append(\n                Fruit(id, seed, group, sort, require, text, grow_up)\n            )\n\n        return matatabi\n\n    def get_names(self) -> list[str | None] | None:\n        if self.matatabi is None:\n            return None\n\n        ids = [fruit.id for fruit in self.matatabi]\n        names = [self.gatya_item_names.get_name(id) for id in ids]\n        return names\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/medals.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass Medals:\n    def __init__(\n        self,\n        u1: int,\n        u2: int,\n        u3: int,\n        medal_data_1: list[int],\n        medal_data_2: dict[int, int],\n        ub: bool,\n    ):\n        self.u1 = u1\n        self.u2 = u2\n        self.u3 = u3\n        self.medal_data_1 = medal_data_1\n        self.medal_data_2 = medal_data_2\n        self.ub = ub\n\n    @staticmethod\n    def init() -> Medals:\n        return Medals(0, 0, 0, [], {}, False)\n\n    @staticmethod\n    def read(data: core.Data) -> Medals:\n        u1 = data.read_int()\n        u2 = data.read_int()\n        u3 = data.read_int()\n        total_medals = data.read_short()\n        medal_data_1 = data.read_short_list(total_medals)\n        total_medals = data.read_short()\n        medal_data_2: dict[int, int] = {}\n        for _ in range(total_medals):\n            key = data.read_short()\n            value = data.read_byte()\n            medal_data_2[key] = value\n        ub = data.read_bool()\n        return Medals(u1, u2, u3, medal_data_1, medal_data_2, ub)\n\n    def write(self, data: core.Data) -> None:\n        data.write_int(self.u1)\n        data.write_int(self.u2)\n        data.write_int(self.u3)\n        data.write_short(len(self.medal_data_1))\n        data.write_short_list(self.medal_data_1, write_length=False)\n        data.write_short(len(self.medal_data_2))\n        for key, value in self.medal_data_2.items():\n            data.write_short(key)\n            data.write_byte(value)\n        data.write_bool(self.ub)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"u1\": self.u1,\n            \"u2\": self.u2,\n            \"u3\": self.u3,\n            \"medal_data_1\": self.medal_data_1,\n            \"medal_data_2\": self.medal_data_2,\n            \"ub\": self.ub,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Medals:\n        return Medals(\n            data.get(\"u1\", 0),\n            data.get(\"u2\", 0),\n            data.get(\"u3\", 0),\n            data.get(\"medal_data_1\", []),\n            data.get(\"medal_data_2\", {}),\n            data.get(\"ub\", False),\n        )\n\n    def __repr__(self) -> str:\n        return (\n            f\"Medals(u1={self.u1}, u2={self.u2}, u3={self.u3}, \"\n            f\"medal_data_1={self.medal_data_1}, medal_data_2={self.medal_data_2}, \"\n            f\"ub={self.ub})\"\n        )\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def has_medal(self, medal_id: int) -> bool:\n        return medal_id in self.medal_data_1\n\n    @staticmethod\n    def edit_medals(save_file: core.SaveFile):\n        medals = save_file.medals\n        medal_names = core.core_data.get_medal_names(save_file)\n        if medal_names.medal_names is None:\n            return\n        options = [\"add_medals\", \"remove_medals\"]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"medal_add_remove_dialog\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        add_medals = choice == 0\n\n        medals_to_choose_from: list[tuple[int, str]] = []\n        for i, medal in enumerate(medal_names.medal_names):\n            if len(medal) == 0:\n                continue\n            if medals.has_medal(i) == add_medals:\n                continue\n            key = \"medal_string\"\n            string = core.core_data.local_manager.get_key(\n                key, medal_name=medal[0], medal_req=medal[1]\n            )\n            medals_to_choose_from.append((i, string))\n        if len(medals_to_choose_from) == 0:\n            return\n        options = [medal[1] for medal in medals_to_choose_from]\n        choices, _ = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"select_medals\"\n        ).multiple_choice()\n        if choices is None:\n            return\n        for choice in choices:\n            medal_id = medals_to_choose_from[choice][0]\n            if add_medals:\n                medals.add_medal(medal_id)\n            else:\n                medals.remove_medal(medal_id)\n\n        if add_medals:\n            color.ColoredText.localize(\"medals_added\")\n        else:\n            color.ColoredText.localize(\"medals_removed\")\n\n    def add_medal(self, medal_id: int) -> None:\n        if self.has_medal(medal_id):\n            return\n        self.medal_data_1.append(medal_id)\n        self.medal_data_2[medal_id] = 0\n\n    def remove_medal(self, medal_id: int) -> None:\n        if medal_id in self.medal_data_2:\n            del self.medal_data_2[medal_id]\n        if medal_id in self.medal_data_1:\n            self.medal_data_1.remove(medal_id)\n\n\nclass MedalNames:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.medal_names = self.get_medal_names()\n\n    def get_medal_names(self) -> list[list[str]] | None:\n        file_name = \"medalname.tsv\"\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"resLocal\", file_name)\n        if data is None:\n            return None\n        csv = core.CSV(data, delimiter=\"\\t\")\n        names: list[list[str]] = []\n        for row in csv:\n            names.append(row.to_str_list())\n        return names\n\n    def get_medal_name(self, medal_id: int) -> list[str] | None:\n        if self.medal_names is None:\n            return None\n        if medal_id < 0 or medal_id >= len(self.medal_names):\n            return []\n        return self.medal_names[medal_id]\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/mission.py",
    "content": "from __future__ import annotations\nfrom typing import Any\n\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass Mission:\n    def __init__(\n        self,\n        clear_state: int | None = None,\n        requirement: int | None = None,\n        progress_type: int | None = None,\n        gamatoto_value: int | None = None,\n        nyancombo_value: int | None = None,\n        user_rank_value: int | None = None,\n        expiry_value: int | None = None,\n        preparing_value: int | bool | None = None,\n    ):\n        self.clear_state = clear_state\n        self.requirement = requirement\n        self.progress_type = progress_type\n        self.gamatoto_value = gamatoto_value\n        self.nyancombo_value = nyancombo_value\n        self.user_rank_value = user_rank_value\n        self.expiry_value = expiry_value\n        self.preparing_value = preparing_value\n\n    @staticmethod\n    def init() -> Mission:\n        return Mission(\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n        )\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"clear_state\": self.clear_state,\n            \"requirement\": self.requirement,\n            \"progress_type\": self.progress_type,\n            \"gamatoto_value\": self.gamatoto_value,\n            \"nyancombo_value\": self.nyancombo_value,\n            \"user_rank_value\": self.user_rank_value,\n            \"expiry_value\": self.expiry_value,\n            \"preparing_value\": self.preparing_value,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Mission:\n        return Mission(\n            data[\"clear_state\"],\n            data[\"requirement\"],\n            data[\"progress_type\"],\n            data[\"gamatoto_value\"],\n            data[\"nyancombo_value\"],\n            data[\"user_rank_value\"],\n            data[\"expiry_value\"],\n            data[\"preparing_value\"],\n        )\n\n    def __repr__(self):\n        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})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass Missions:\n    def __init__(\n        self,\n        clear_states: dict[int, int],\n        requirements: dict[int, int],\n        progress_types: dict[int, int],\n        gamatoto_values: dict[int, int],\n        nyancombo_values: dict[int, int],\n        user_rank_values: dict[int, int],\n        expiry_values: dict[int, int],\n        preparing_values: dict[int, int | bool],\n    ):\n        self.clear_states = clear_states\n        self.requirements = requirements\n        self.progress_types = progress_types\n        self.gamatoto_values = gamatoto_values\n        self.nyancombo_values = nyancombo_values\n        self.user_rank_values = user_rank_values\n        self.expiry_values = expiry_values\n        self.preparing_values = preparing_values\n        self.weekly_missions: dict[int, bool] = {}\n\n    @staticmethod\n    def init() -> Missions:\n        return Missions(\n            {},\n            {},\n            {},\n            {},\n            {},\n            {},\n            {},\n            {},\n        )\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> Missions:\n        clear_states: dict[int, int] = stream.read_int_int_dict()\n        requirements: dict[int, int] = stream.read_int_int_dict()\n        progress_types: dict[int, int] = stream.read_int_int_dict()\n        gamatoto_values: dict[int, int] = stream.read_int_int_dict()\n        nyancombo_values: dict[int, int] = stream.read_int_int_dict()\n        user_rank_values: dict[int, int] = stream.read_int_int_dict()\n        expiry_values: dict[int, int] = stream.read_int_int_dict()\n        preparing_values: dict[int, int | bool] = {}\n\n        for _ in range(stream.read_int()):\n            key = stream.read_int()\n            if gv < 90300:\n                preparing_values[key] = stream.read_bool()\n            else:\n                preparing_values[key] = stream.read_int()\n\n        return Missions(\n            clear_states,\n            requirements,\n            progress_types,\n            gamatoto_values,\n            nyancombo_values,\n            user_rank_values,\n            expiry_values,\n            preparing_values,\n        )\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        stream.write_int_int_dict(self.clear_states)\n        stream.write_int_int_dict(self.requirements)\n        stream.write_int_int_dict(self.progress_types)\n        stream.write_int_int_dict(self.gamatoto_values)\n        stream.write_int_int_dict(self.nyancombo_values)\n        stream.write_int_int_dict(self.user_rank_values)\n        stream.write_int_int_dict(self.expiry_values)\n\n        stream.write_int(len(self.preparing_values))\n        for key, value in self.preparing_values.items():\n            stream.write_int(key)\n            if gv < 90300:\n                stream.write_bool(bool(value))\n            else:\n                stream.write_int(int(value))\n\n    def read_weekly_missions(self, stream: core.Data):\n        self.weekly_missions: dict[int, bool] = {}\n        for _ in range(stream.read_int()):\n            key = stream.read_int()\n            self.weekly_missions[key] = stream.read_bool()\n\n    def write_weekly_missions(self, stream: core.Data):\n        stream.write_int(len(self.weekly_missions))\n        for key, value in self.weekly_missions.items():\n            stream.write_int(key)\n            stream.write_bool(value)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"clear_states\": self.clear_states,\n            \"requirements\": self.requirements,\n            \"progress_types\": self.progress_types,\n            \"gamatoto_values\": self.gamatoto_values,\n            \"nyancombo_values\": self.nyancombo_values,\n            \"user_rank_values\": self.user_rank_values,\n            \"expiry_values\": self.expiry_values,\n            \"preparing_values\": self.preparing_values,\n            \"weekly_missions\": self.weekly_missions,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]):\n        missions = Missions(\n            data.get(\"clear_states\", {}),\n            data.get(\"requirements\", {}),\n            data.get(\"progress_types\", {}),\n            data.get(\"gamatoto_values\", {}),\n            data.get(\"nyancombo_values\", {}),\n            data.get(\"user_rank_values\", {}),\n            data.get(\"expiry_values\", {}),\n            data.get(\"preparing_values\", {}),\n        )\n        missions.weekly_missions = data.get(\"weekly_missions\", {})\n        return missions\n\n    def __repr__(self):\n        return f\"<Missions {self.serialize()}>\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    @staticmethod\n    def edit_missions(save_file: core.SaveFile):\n        missions = save_file.missions\n\n        names = core.core_data.get_mission_names(save_file)\n        conditions = core.core_data.get_mission_conditions(save_file)\n        if names.names is None or conditions.conditions is None:\n            return\n        options: list[str] = []\n        mssion_ids: list[int] = []\n        for mission_id, name in names.names.items():\n            if mission_id in missions.clear_states:\n                name = name.split(\"<br>\")[0]\n                condition = conditions.conditions.get(mission_id)\n                if not condition:\n                    continue\n                name = name.replace(\"%d\", str(condition.progress_count))\n                if \"%@\" in name and len(condition.conditions_value) > 2:\n                    name = name.replace(\n                        \"%@\", str(condition.conditions_value[2])\n                    )\n                options.append(name)\n                mssion_ids.append(mission_id)\n\n        re_claim = dialog_creator.ChoiceInput.from_reduced(\n            [\"complete_reward\", \"complete_claim\", \"uncomplete\"],\n            dialog=\"select_mission_claim\",\n            single_choice=True,\n        ).single_choice()\n        if re_claim is None:\n            return\n        re_claim -= 1\n\n        choices, _ = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"select_missions\"\n        ).multiple_choice(localized_options=False)\n        if choices is None:\n            return\n        for choice in choices:\n            mission_id = mssion_ids[choice]\n            if re_claim == 0:\n                missions.clear_states[mission_id] = 2\n                condition = conditions.get_condition(mission_id)\n                if condition is not None:\n                    missions.requirements[mission_id] = condition.progress_count\n            elif re_claim == 1:\n                missions.clear_states[mission_id] = 4\n                condition = conditions.get_condition(mission_id)\n                if condition is not None:\n                    missions.requirements[mission_id] = condition.progress_count\n            elif re_claim == 2:\n                missions.clear_states[mission_id] = 0\n                if mission_id in missions.requirements:\n                    missions.requirements[mission_id] = 0\n\n        color.ColoredText.localize(\"missions_edited\")\n\n\nclass MissionCondition:\n    def __init__(\n        self,\n        mission_id: int,\n        mission_type: int,\n        conditions_type: int,\n        progress_count: int,\n        conditions_value: list[int],\n    ):\n        self.mission_id = mission_id\n        self.mission_type = mission_type\n        self.conditions_type = conditions_type\n        self.progress_count = progress_count\n        self.conditions_value = conditions_value\n\n\nclass MissionConditions:\n    def __init__(self, save: core.SaveFile):\n        self.save = save\n        self.conditions = self.get_conditions()\n\n    def get_conditions(self) -> dict[int, MissionCondition] | None:\n        file_name = \"Mission_Condition.csv\"\n        gdg = core.core_data.get_game_data_getter(self.save)\n        file = gdg.download(\"DataLocal\", file_name)\n        if file is None:\n            return None\n        csv = core.CSV(file)\n        conditions: dict[int, MissionCondition] = {}\n        for row in csv:\n            conditions[row[0].to_int()] = MissionCondition(\n                row[0].to_int(),\n                row[1].to_int(),\n                row[2].to_int(),\n                row[3].to_int(),\n                row[4:].to_int_list(),\n            )\n        return conditions\n\n    def get_condition(self, mission_id: int) -> MissionCondition | None:\n        if self.conditions is None:\n            return None\n        return self.conditions.get(mission_id)\n\n\nclass MissionNames:\n    def __init__(self, save: core.SaveFile):\n        self.save = save\n        self.names = self.get_names()\n\n    def get_names(self) -> dict[int, str] | None:\n        file_name = \"Mission_Name.csv\"\n        gdg = core.core_data.get_game_data_getter(self.save)\n        file = gdg.download(\"resLocal\", file_name)\n        if file is None:\n            return None\n        csv = core.CSV(\n            file, delimiter=core.Delimeter.from_country_code_res(self.save.cc)\n        )\n        names: dict[int, str] = {}\n        for row in csv:\n            names[row[0].to_int()] = row[1].to_str()\n        return names\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/my_sale.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass MySale:\n    def __init__(self, dict_1: dict[int, int], dict_2: dict[int, bool]):\n        self.dict_1 = dict_1\n        self.dict_2 = dict_2\n\n    @staticmethod\n    def init() -> MySale:\n        return MySale({}, {})\n\n    @staticmethod\n    def read_bonus_hash(stream: core.Data):\n        variable_length = stream.read_variable_length_int()\n        dict_1 = {}\n        for _ in range(variable_length):\n            key = stream.read_variable_length_int()\n            value = stream.read_variable_length_int()\n            dict_1[key] = value\n\n        variable_length = stream.read_variable_length_int()\n        dict_2 = {}\n        for _ in range(variable_length):\n            key = stream.read_variable_length_int()\n            value = stream.read_byte()\n            dict_2[key] = value\n\n        return MySale(dict_1, dict_2)\n\n    def write_bonus_hash(self, stream: core.Data):\n        stream.write_variable_length_int(len(self.dict_1))\n        for key, value in self.dict_1.items():\n            stream.write_variable_length_int(key)\n            stream.write_variable_length_int(value)\n\n        stream.write_variable_length_int(len(self.dict_2))\n        for key, value in self.dict_2.items():\n            stream.write_variable_length_int(key)\n            stream.write_byte(value)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"dict_1\": self.dict_1,\n            \"dict_2\": self.dict_2,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> MySale:\n        return MySale(data.get(\"dict_1\", {}), data.get(\"dict_2\", {}))\n\n    def __repr__(self) -> str:\n        return f\"MySale(dict_1={self.dict_1}, dict_2={self.dict_2})\"\n\n    def __str__(self) -> str:\n        return f\"MySale(dict_1={self.dict_1}, dict_2={self.dict_2})\"\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/nyanko_club.py",
    "content": "from __future__ import annotations\nimport datetime\nimport random\nimport time\nfrom typing import Any\n\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass NyankoClub:\n    def __init__(\n        self,\n        officer_id: int,\n        total_renewal_times: int,\n        start_date_now: float,\n        end_date_now: float,\n        start_date_next: float,\n        end_date_next: float,\n        start_date_total: float,\n        end_date_total: float,\n        time_error_end: float,\n        total_state_updates: int,\n        login_bonus_date: float,\n        claimed_rewards: dict[int, int],\n        remaing_days_popup: float,\n        first_popup_flag: bool,\n        badge_flag: bool | None = None,\n    ):\n        self.officer_id = officer_id\n        self.total_renewal_times = total_renewal_times\n        self.start_date_now = start_date_now\n        self.end_date_now = end_date_now\n        self.start_date_next = start_date_next\n        self.end_date_next = end_date_next\n        self.start_date_total = start_date_total\n        self.end_date_total = end_date_total\n        self.time_error_end = time_error_end\n        self.total_state_updates = total_state_updates\n        self.login_bonus_date = login_bonus_date\n        self.claimed_rewards = claimed_rewards\n        self.remaing_days_popup = remaing_days_popup\n        self.first_popup_flag = first_popup_flag\n        self.badge_flag = badge_flag\n\n    @staticmethod\n    def init() -> NyankoClub:\n        return NyankoClub(\n            0,\n            0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n            0,\n            0.0,\n            {},\n            0.0,\n            False,\n            False,\n        )\n\n    @staticmethod\n    def read(data: core.Data, gv: core.GameVersion) -> NyankoClub:\n        officer_id = data.read_int()\n        total_renewal_times = data.read_int()\n        start_date_now = data.read_double()\n        end_date_now = data.read_double()\n        start_date_next = data.read_double()\n        end_date_next = data.read_double()\n        start_date_total = data.read_double()\n        end_date_total = data.read_double()\n        time_error_end = data.read_double()\n        total_state_updates = data.read_int()\n        login_bonus_date = data.read_double()\n        claimed_rewards = data.read_int_int_dict()\n        remaing_days_popup = data.read_double()\n        first_popup_flag = data.read_bool()\n        if gv >= 80100:\n            badge_flag = data.read_bool()\n        else:\n            badge_flag = None\n        return NyankoClub(\n            officer_id,\n            total_renewal_times,\n            start_date_now,\n            end_date_now,\n            start_date_next,\n            end_date_next,\n            start_date_total,\n            end_date_total,\n            time_error_end,\n            total_state_updates,\n            login_bonus_date,\n            claimed_rewards,\n            remaing_days_popup,\n            first_popup_flag,\n            badge_flag,\n        )\n\n    def write(self, data: core.Data, gv: core.GameVersion):\n        data.write_int(self.officer_id)\n        data.write_int(self.total_renewal_times)\n        data.write_double(self.start_date_now)\n        data.write_double(self.end_date_now)\n        data.write_double(self.start_date_next)\n        data.write_double(self.end_date_next)\n        data.write_double(self.start_date_total)\n        data.write_double(self.end_date_total)\n        data.write_double(self.time_error_end)\n        data.write_int(self.total_state_updates)\n        data.write_double(self.login_bonus_date)\n        data.write_int_int_dict(self.claimed_rewards)\n        data.write_double(self.remaing_days_popup)\n        data.write_bool(self.first_popup_flag)\n        if gv >= 80100:\n            data.write_bool(self.badge_flag or False)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"officer_id\": self.officer_id,\n            \"total_renewal_times\": self.total_renewal_times,\n            \"start_date_now\": self.start_date_now,\n            \"end_date_now\": self.end_date_now,\n            \"start_date_next\": self.start_date_next,\n            \"end_date_next\": self.end_date_next,\n            \"start_date_total\": self.start_date_total,\n            \"end_date_total\": self.end_date_total,\n            \"time_error_end\": self.time_error_end,\n            \"total_state_updates\": self.total_state_updates,\n            \"login_bonus_date\": self.login_bonus_date,\n            \"claimed_rewards\": self.claimed_rewards,\n            \"remaing_days_popup\": self.remaing_days_popup,\n            \"first_popup_flag\": self.first_popup_flag,\n            \"badge_flag\": self.badge_flag,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> NyankoClub:\n        return NyankoClub(\n            data.get(\"officer_id\", 0),\n            data.get(\"total_renewal_times\", 0),\n            data.get(\"start_date_now\", 0.0),\n            data.get(\"end_date_now\", 0.0),\n            data.get(\"start_date_next\", 0.0),\n            data.get(\"end_date_next\", 0.0),\n            data.get(\"start_date_total\", 0.0),\n            data.get(\"end_date_total\", 0.0),\n            data.get(\"time_error_end\", 0.0),\n            data.get(\"total_state_updates\", 0),\n            data.get(\"login_bonus_date\", 0.0),\n            data.get(\"claimed_rewards\", {}),\n            data.get(\"remaing_days_popup\", 0.0),\n            data.get(\"first_popup_flag\", False),\n            data.get(\"badge_flag\", False),\n        )\n\n    def __repr__(self):\n        return f\"<NyankoClub {self.officer_id}>\"\n\n    def __str__(self):\n        return f\"NyankoClub {self.officer_id}\"\n\n    def get_gold_pass(\n        self, officer_id: int, total_days: int, save_file: core.SaveFile\n    ):\n        self.officer_id = officer_id\n        start_date_now = int(time.time())\n        end_date_now = (\n            start_date_now + datetime.timedelta(days=total_days).total_seconds()\n        )\n        end_date_total = (\n            start_date_now\n            + datetime.timedelta(days=total_days * 2).total_seconds()\n        )\n\n        self.total_renewal_times = 2\n        self.start_date_now = start_date_now\n        self.end_date_now = end_date_now\n\n        self.start_date_next = end_date_now\n        self.end_date_next = end_date_total\n\n        self.start_date_total = start_date_now\n        self.end_date_total = end_date_total\n\n        self.time_error_end = start_date_now\n\n        self.total_state_updates = 2\n\n        self.login_bonus_date = end_date_now\n\n        self.remaing_days_popup = 0.0\n        self.first_popup_flag = True\n        self.badge_flag = False\n\n        login = save_file.logins.get_login(5100)\n        if login is not None:\n            login.count = 0\n\n        self.claimed_rewards = {}\n\n    def remove_gold_pass(self, save_file: core.SaveFile):\n        self.officer_id = -1\n        self.total_renewal_times = 0\n        self.start_date_now = 0.0\n        self.end_date_now = 0.0\n        self.start_date_next = 0.0\n        self.end_date_next = 0.0\n        self.start_date_total = 0.0\n        self.end_date_total = 0.0\n        self.time_error_end = 0.0\n        self.total_state_updates = 0\n        self.login_bonus_date = 0.0\n        self.remaing_days_popup = 0.0\n        self.first_popup_flag = False\n        self.badge_flag = False\n\n        login = save_file.logins.get_login(5100)\n        if login is not None:\n            login.count = 0\n\n        self.claimed_rewards = {}\n\n    @staticmethod\n    def get_random_officer_id() -> int:\n        return random.randint(1, 2**16 - 1)\n\n    @staticmethod\n    def edit_gold_pass(save_file: core.SaveFile):\n        club = save_file.officer_pass.gold_pass\n\n        officer_id = color.ColoredInput().localize(\"gold_pass_dialog\").strip()\n        if not officer_id:\n            officer_id = NyankoClub.get_random_officer_id()\n\n        if officer_id == \"-1\":\n            officer_id = -1\n        else:\n            try:\n                officer_id = int(officer_id)\n            except ValueError:\n                officer_id = NyankoClub.get_random_officer_id()\n            officer_id = dialog_creator.IntInput().clamp_value(officer_id)\n\n        if officer_id == -1:\n            club.remove_gold_pass(save_file)\n            color.ColoredText.localize(\"gold_pass_remove_success\")\n        else:\n            club.get_gold_pass(officer_id, 30, save_file)\n            color.ColoredText.localize(\"gold_pass_get_success\", id=officer_id)\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/officer_pass.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\nclass OfficerPass:\n    def __init__(self, play_time: int):\n        self.play_time = play_time\n        self.gold_pass = core.NyankoClub.init()\n        self.cat_id = 0\n        self.cat_form = 0\n\n    @staticmethod\n    def init() -> OfficerPass:\n        return OfficerPass(0)\n\n    @staticmethod\n    def read(data: core.Data) -> OfficerPass:\n        play_time = data.read_int()\n        return OfficerPass(play_time)\n\n    def write(self, data: core.Data):\n        if self.play_time > 2**31 - 1:\n            self.play_time = 2**31 - 1\n        data.write_int(self.play_time)\n\n    def read_gold_pass(self, data: core.Data, gv: core.GameVersion):\n        self.gold_pass = core.NyankoClub.read(data, gv)\n\n    def write_gold_pass(self, data: core.Data, gv: core.GameVersion):\n        self.gold_pass.write(data, gv)\n\n    def read_cat_data(self, data: core.Data):\n        self.cat_id = data.read_short()\n        self.cat_form = data.read_short()\n\n    def write_cat_data(self, data: core.Data):\n        data.write_short(self.cat_id)\n        data.write_short(self.cat_form)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"play_time\": self.play_time,\n            \"gold_pass\": self.gold_pass.serialize(),\n            \"cat_id\": self.cat_id,\n            \"cat_form\": self.cat_form,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> OfficerPass:\n        officer_pass = OfficerPass(\n            data.get(\"play_time\", 0),\n        )\n        officer_pass.gold_pass = core.NyankoClub.deserialize(\n            data.get(\"gold_pass\", {})\n        )\n        officer_pass.cat_id = data.get(\"cat_id\", 0)\n        officer_pass.cat_form = data.get(\"cat_form\", 0)\n        return officer_pass\n\n    def __repr__(self):\n        return f\"OfficerPass({self.play_time}, {self.gold_pass}, {self.cat_id}, {self.cat_form})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def reset(self, save_file: core.SaveFile):\n        self.cat_id = 0\n        self.cat_form = 0\n        self.play_time = 0\n        self.gold_pass.remove_gold_pass(save_file)\n\n    @staticmethod\n    def fix_crash(save_file: core.SaveFile):\n        officer_pass = save_file.officer_pass\n        officer_pass.reset(save_file)\n\n        color.ColoredText.localize(\"officer_pass_fixed\")\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/playtime.py",
    "content": "from __future__ import annotations\nfrom dataclasses import dataclass\n\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\n@dataclass\nclass PlayTime:\n    frames: int\n\n    @staticmethod\n    def get_fps() -> int:\n        return 30\n\n    @property\n    def seconds(self) -> int:\n        return self.frames // self.get_fps()\n\n    @property\n    def minutes(self) -> int:\n        return self.seconds // 60\n\n    @property\n    def hours(self) -> int:\n        return self.minutes // 60\n\n    @property\n    def just_seconds(self) -> int:\n        return self.seconds % 60\n\n    @property\n    def just_minutes(self) -> int:\n        return self.minutes % 60\n\n    @property\n    def just_hours(self) -> int:\n        return self.hours % 60\n\n    @staticmethod\n    def from_hours(hours: int) -> PlayTime:\n        return PlayTime(hours * 60 * 60 * PlayTime.get_fps())\n\n    @staticmethod\n    def from_minutes(minutes: int) -> PlayTime:\n        return PlayTime(minutes * 60 * PlayTime.get_fps())\n\n    @staticmethod\n    def from_seconds(seconds: int) -> PlayTime:\n        return PlayTime(seconds * PlayTime.get_fps())\n\n    @staticmethod\n    def from_hours_mins_secs(\n        hours: int, minutes: int, seconds: int\n    ) -> PlayTime:\n        return (\n            PlayTime.from_hours(hours)\n            + PlayTime.from_minutes(minutes)\n            + PlayTime.from_seconds(seconds)\n        )\n\n    def __add__(self, other: PlayTime) -> PlayTime:\n        return PlayTime(self.frames + other.frames)\n\n\ndef edit(save_file: core.SaveFile):\n    play_time = PlayTime(save_file.officer_pass.play_time)\n    color.ColoredText.localize(\n        \"playtime_current\",\n        hours=play_time.hours,\n        minutes=play_time.just_minutes,\n        seconds=play_time.just_seconds,\n        frames=play_time.frames,\n    )\n    hours, _ = dialog_creator.IntInput().get_input(\"playtime_hours_prompt\", {})\n    if hours is None:\n        return\n    minutes, _ = dialog_creator.IntInput().get_input(\n        \"playtime_minutes_prompt\", {}\n    )\n    if minutes is None:\n        return\n    seconds, _ = dialog_creator.IntInput().get_input(\n        \"playtime_seconds_prompt\", {}\n    )\n    if seconds is None:\n        return\n\n    play_time = PlayTime.from_hours_mins_secs(hours, minutes, seconds)\n    save_file.officer_pass.play_time = play_time.frames\n    color.ColoredText.localize(\n        \"playtime_edited\",\n        hours=play_time.hours,\n        minutes=play_time.just_minutes,\n        seconds=play_time.just_seconds,\n        frames=play_time.frames,\n    )\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/powerup.py",
    "content": "from __future__ import annotations\n\nfrom bcsfe import core\n\n\nclass PowerUpHelper:\n    def __init__(self, cat: core.Cat, save_file: core.SaveFile):\n        self.cat = cat\n        self.save_file = save_file\n        self.unit_limit = self.save_file.cats.read_unitlimit(\n            self.save_file\n        ).get_unit_limit(self.cat.id)\n        self.all_unit_buy = self.save_file.cats.read_unitbuy(self.save_file)\n        self.unit_buy = self.all_unit_buy.get_unit_buy(self.cat.id)\n        self.rank_gifts = self.save_file.user_rank_rewards.read_rank_gifts(\n            self.save_file\n        )\n        self.max_upgrade_level = self.__get_max_upgrade_level_check()\n\n    def get_current_max_level(self) -> int | None:\n        if self.unit_buy is None:\n            return None\n        return min(\n            self.unit_buy.original_max_levels[0] + self.max_upgrade_level,\n            self.unit_buy.max_upgrade_level_catseye,\n        )\n\n    def has_strict_upgrade(self) -> bool:\n        return core.core_data.config.get_bool(core.ConfigKey.STRICT_UPGRADE)\n\n    def get_upgrade_state_check(self) -> int:\n        if not self.has_strict_upgrade():\n            return 100000\n        return self.save_file.upgrade_state\n\n    def get_user_rank_check(self) -> int:\n        if not self.has_strict_upgrade():\n            return 1000000\n        return self.save_file.calculate_user_rank()\n\n    def __get_max_upgrade_level_check(self) -> int:\n        if self.unit_limit is None:\n            return self.cat.max_upgrade_level.base\n\n        rewards = self.save_file.user_rank_rewards\n        self.cat.max_upgrade_level.reset()\n\n        strict_upgrade = self.has_strict_upgrade()\n\n        for reward_id in range(len(rewards.rewards)):\n            rank_gift = self.rank_gifts.get_by_id(reward_id)\n            if rank_gift is None:\n                continue\n            user_rank_reward = rewards.rewards[reward_id]\n            if not user_rank_reward.claimed and strict_upgrade:\n                continue\n            for present in rank_gift.rewards:\n                if present[0] >= 1000 and present[0] <= 1599:\n                    for limit in self.unit_limit.values:\n                        if limit == present[0]:\n                            self.cat.max_upgrade_level.increment_base(\n                                present[1]\n                            )\n                elif present[0] >= 4000 and present[0] <= 4599:\n                    for limit in self.unit_limit.values:\n                        if limit == present[0]:\n                            self.cat.max_upgrade_level.increment_plus(\n                                present[1]\n                            )\n\n        return self.cat.max_upgrade_level.base\n\n    def can_power_up(self) -> bool:\n        if self.unit_buy is None:\n            return False\n        base_level = self.cat.upgrade.get_base()\n        current_max_level = self.get_current_max_level()\n        if current_max_level is None:\n            return False\n\n        if base_level >= current_max_level or (\n            (\n                self.get_upgrade_state_check() > 1\n                or base_level == self.unit_buy.unknown_22\n            )\n            and self.get_upgrade_state_check() < 2\n        ):\n            return (\n                self.unit_buy.rarity != 0\n                and base_level >= self.unit_buy.max_upgrade_level_no_catseye\n                and base_level < self.unit_buy.max_upgrade_level_catseye\n                and base_level < current_max_level\n            )\n        return True\n\n    def can_use_catseye(self) -> bool:\n        if self.unit_buy is None:\n            return False\n\n        base_level = self.cat.upgrade.get_base()\n        return (\n            self.unit_buy.rarity != 0\n            and base_level >= self.unit_buy.max_upgrade_level_no_catseye\n            and self.unit_buy.max_upgrade_level_no_catseye != -1\n            and self.get_user_rank_check() >= 1600\n        )\n\n    def upgrade_cat(self, force: bool = False) -> bool:\n        if force:\n            self.cat.upgrade_base(self.save_file)\n            return True\n        if self.unit_buy is None:\n            return False\n        current_max_level = self.get_current_max_level()\n        if current_max_level is None:\n            return False\n\n        if self.can_power_up():\n            self.cat.upgrade_base(self.save_file)\n            return True\n\n        if (\n            self.can_use_catseye()\n            and self.unit_buy.max_upgrade_level_no_catseye <= current_max_level\n        ):\n            if (\n                self.cat.upgrade.get_base()\n                < self.unit_buy.max_upgrade_level_catseye\n            ):\n                self.cat.upgrade_base(self.save_file)\n                self.cat.catseyes_used += 1\n                self.cat.max_upgrade_level.upgrade()\n                return True\n            return False\n        return False\n\n    def get_max_max_base_upgrade_level(self) -> int:\n        max_level = 0\n        if self.all_unit_buy.unit_buy is None:\n            return 90\n        for unit_buy in self.all_unit_buy.unit_buy:\n            if unit_buy.max_upgrade_level_catseye > max_level:\n                max_level = unit_buy.max_upgrade_level_catseye\n        return max_level\n\n    def get_max_max_plus_upgrade_level(self) -> int:\n        max_level = 0\n        if self.all_unit_buy.unit_buy is None:\n            return 90\n        for unit_buy in self.all_unit_buy.unit_buy:\n            if unit_buy.max_plus_upgrade_level > max_level:\n                max_level = unit_buy.max_plus_upgrade_level\n        return max_level\n\n    def get_max_possible_base(self) -> int:\n        if self.unit_buy is None:\n            return 90\n        return self.unit_buy.max_upgrade_level_catseye\n\n    def get_max_possible_plus(self) -> int:\n        if self.unit_buy is None:\n            return 90\n        return self.unit_buy.max_plus_upgrade_level\n\n    def reset_upgrade(self):\n        self.cat.upgrade.base = 0\n        self.cat.catseyes_used = 0\n\n    def upgrade_by(self, amount: int):\n        if amount == -1:\n            return\n        for _ in range(amount):\n            did_upgrade = self.upgrade_cat()\n            if not did_upgrade:\n                break\n\n    def max_upgrade(self):\n        while self.upgrade_cat():\n            pass\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/scheme_items.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass SchemeDataItem:\n    def __init__(\n        self,\n        id: int,\n        type: int,\n        type_id: int,\n        item_id: int,\n        number: int,\n        type_id2: int | None = None,\n        item_id2: int | None = None,\n        number2: int | None = None,\n        type_id3: int | None = None,\n        item_id3: int | None = None,\n        number3: int | None = None,\n    ):\n        self.id = id\n        self.type = type\n        self.type_id = type_id\n        self.item_id = item_id\n        self.number = number\n        self.type_id2 = type_id2\n        self.item_id2 = item_id2\n        self.number2 = number2\n        self.type_id3 = type_id3\n        self.item_id3 = item_id3\n        self.number3 = number3\n\n    def is_cat(self) -> bool:\n        return self.type_id == 1\n\n    def get_name(self, localizable: core.Localizable) -> str | None:\n        key = f\"scheme_popup_{self.id}\"\n        name = localizable.get(key)\n        if name is None:\n            return None\n        return name.replace(\"<flash>,\", \"\").replace(\"<flash>\", \"\")\n\n\nclass SchemeItems:\n    def __init__(self, to_obtain: list[int], received: list[int]):\n        self.to_obtain = to_obtain\n        self.received = received\n\n    @staticmethod\n    def init() -> SchemeItems:\n        return SchemeItems([], [])\n\n    @staticmethod\n    def read(stream: core.Data) -> SchemeItems:\n        total = stream.read_int()\n        to_obtain: list[int] = []\n        for _ in range(total):\n            to_obtain.append(stream.read_int())\n\n        total = stream.read_int()\n        received: list[int] = []\n        for _ in range(total):\n            received.append(stream.read_int())\n\n        return SchemeItems(to_obtain, received)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.to_obtain))\n        for item in self.to_obtain:\n            stream.write_int(item)\n\n        stream.write_int(len(self.received))\n        for item in self.received:\n            stream.write_int(item)\n\n    def serialize(self) -> dict[str, list[int]]:\n        return {\"to_obtain\": self.to_obtain, \"received\": self.received}\n\n    @staticmethod\n    def deserialize(data: dict[str, list[int]]) -> SchemeItems:\n        return SchemeItems(data.get(\"to_obtain\", []), data.get(\"received\", []))\n\n    def __repr__(self) -> str:\n        return f\"SchemeItems(to_obtain={self.to_obtain!r}, received={self.received!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def edit(self, save_file: core.SaveFile):\n        item_names = core.core_data.get_gatya_item_names(save_file)\n        localizable = save_file.get_localizable()\n        scheme_data = core.core_data.get_game_data_getter(save_file).download(\n            \"DataLocal\", \"schemeItemData.tsv\"\n        )\n        if scheme_data is None:\n            return\n        csv = core.CSV(scheme_data, \"\\t\")\n        scheme_items: dict[int, SchemeDataItem] = {}\n        for line in csv.lines[1:]:\n            scheme_items[line[0].to_int()] = SchemeDataItem(\n                line[0].to_int(),\n                line[1].to_int(),\n                line[2].to_int(),\n                line[3].to_int(),\n                line[4].to_int(),\n                line[5].to_int(),\n                line[6].to_int(),\n                line[7].to_int(),\n                line[8].to_int(),\n                line[9].to_int(),\n                line[10].to_int(),\n            )\n\n        options: list[str] = []\n        for item in scheme_items.values():\n            scheme_name = item.get_name(localizable)\n            if scheme_name is None:\n                return\n            string = \"\\n\\t\"\n            if item.is_cat():\n                cat_names = core.Cat.get_names(item.item_id, save_file)\n                if cat_names:\n                    cat_name = cat_names[0]\n                    string += scheme_name.replace(\"%@\", cat_name)\n            else:\n                item_name = item_names.get_name(item.item_id)\n                if item_name:\n                    string += scheme_name\n                    first_index = string.find(\"%@\")\n                    second_index = string.find(\"%@\", first_index + 1)\n                    string = (\n                        string[:first_index]\n                        + str(item.number)\n                        + \" \"\n                        + item_name\n                        + string[second_index + 2 :]\n                    )\n            string = string.replace(\"<br>\", \"\\n\\t\")\n            options.append(string)\n\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            [\"gain_scheme_items\", \"remove_scheme_items\"],\n            dialog=\"gain_remove_scheme_items\",\n        ).single_choice()\n        if choice is None:\n            return\n\n        choice -= 1\n\n        if choice == 0:\n            self.add_scheme_items(options, scheme_items)\n        elif choice == 1:\n            self.remove_scheme_items(options, scheme_items)\n\n    def add_scheme_items(\n        self,\n        options: list[str],\n        scheme_items: dict[int, SchemeDataItem],\n    ):\n        scheme_ids, _ = dialog_creator.ChoiceInput.from_reduced(\n            options,\n            dialog=\"scheme_items_select_gain\",\n        ).multiple_choice()\n        if scheme_ids is None:\n            return\n        for option_id in scheme_ids:\n            scheme_id = list(scheme_items.keys())[option_id]\n            if scheme_id not in self.to_obtain:\n                self.to_obtain.append(scheme_id)\n            if scheme_id in self.received:\n                self.received.remove(scheme_id)\n\n        color.ColoredText.localize(\"scheme_items_edit_success\")\n\n    def remove_scheme_items(\n        self,\n        options: list[str],\n        scheme_items: dict[int, SchemeDataItem],\n    ):\n        scheme_ids, _ = dialog_creator.ChoiceInput.from_reduced(\n            options,\n            dialog=\"scheme_items_select_remove\",\n        ).multiple_choice()\n        if scheme_ids is None:\n            return\n        for option_id in scheme_ids:\n            scheme_id = list(scheme_items.keys())[option_id]\n            if scheme_id in self.to_obtain:\n                self.to_obtain.remove(scheme_id)\n            if scheme_id in self.received:\n                self.received.remove(scheme_id)\n\n        color.ColoredText.localize(\"scheme_items_edit_success\")\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/special_skill.py",
    "content": "from __future__ import annotations, division\nfrom bcsfe import core\n\nfrom typing import Any\n\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass SpecialSkill:\n    def __init__(self, upg: core.Upgrade):\n        self.upgrade = upg\n        self.seen = 0\n        self.max_upgrade_level = core.Upgrade(0, 0)\n\n    @staticmethod\n    def init() -> SpecialSkill:\n        return SpecialSkill(core.Upgrade(0, 0))\n\n    @staticmethod\n    def read_upgrade(stream: core.Data) -> SpecialSkill:\n        up = core.Upgrade.read(stream)\n        return SpecialSkill(up)\n\n    def write_upgrade(self, stream: core.Data):\n        self.upgrade.write(stream)\n\n    def read_seen(self, stream: core.Data):\n        self.seen = stream.read_int()\n\n    def write_seen(self, stream: core.Data):\n        stream.write_int(self.seen)\n\n    def read_max_upgrade_level(self, stream: core.Data):\n        level = core.Upgrade.read(stream)\n        self.max_upgrade_level = level\n\n    def write_max_upgrade_level(self, stream: core.Data):\n        self.max_upgrade_level.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"upgrade\": self.upgrade.serialize(),\n            \"seen\": self.seen,\n            \"max_upgrade_level\": self.max_upgrade_level.serialize(),\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> SpecialSkill:\n        skill = SpecialSkill(core.Upgrade.deserialize(data.get(\"upgrade\", {})))\n        skill.seen = data.get(\"seen\", 0)\n        skill.max_upgrade_level = core.Upgrade.deserialize(\n            data.get(\"max_upgrade_level\", {})\n        )\n        return skill\n\n    def __repr__(self) -> str:\n        return f\"Skill(upgrade={self.upgrade}, seen={self.seen}, max_upgrade_level={self.max_upgrade_level})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def set_upgrade(\n        self,\n        upgrade: core.Upgrade,\n        only_plus: bool = False,\n        max_base: int | None = None,\n        max_plus: int | None = None,\n    ):\n        if max_base is not None:\n            upgrade.base = min(upgrade.base, max_base)\n        if max_plus is not None:\n            upgrade.plus = min(upgrade.plus, max_plus)\n\n        base = upgrade.base\n        plus = upgrade.plus\n\n        if base != -1 and not only_plus:\n            self.upgrade.base = upgrade.get_random_base(max_base)\n        if plus != -1:\n            self.upgrade.plus = upgrade.get_random_plus(max_plus)\n\n\nclass SpecialSkills:\n    def __init__(self, skills: list[SpecialSkill]):\n        self.skills = skills\n\n    def get_upgrade(self, valid_skill_id: int) -> SpecialSkill:\n        if valid_skill_id >= 1:\n            valid_skill_id += 1\n\n        return self.skills[valid_skill_id]\n\n    def set_upgrade(\n        self,\n        valid_skill_id: int,\n        upgrade: core.Upgrade,\n        max_base: int | None = None,\n        max_plus: int | None = None,\n    ):\n        u = upgrade.copy()\n        valid_skills = self.get_valid_skills()\n        valid_skills[valid_skill_id].set_upgrade(\n            u, max_base=max_base, max_plus=max_plus\n        )\n\n        if (\n            valid_skill_id == 0\n        ):  # if it is a cat cannon power upgrade, mirror the upgrade to the hidden cat cannon power special skill\n            self.skills[1].set_upgrade(u, max_base=max_base, max_plus=max_plus)\n\n    @staticmethod\n    def init() -> SpecialSkills:\n        skills = [SpecialSkill.init() for _ in range(11)]\n        return SpecialSkills(skills)\n\n    def get_valid_skills(self) -> list[SpecialSkill]:\n        new_skills: list[SpecialSkill] = []\n        for i, skill in enumerate(self.skills):\n            if i == 1:\n                continue\n            new_skills.append(skill)\n\n        return new_skills\n\n    @staticmethod\n    def read_upgrades(stream: core.Data) -> SpecialSkills:\n        total_skills = 11\n\n        skills: list[SpecialSkill] = []\n        for _ in range(total_skills):\n            skills.append(SpecialSkill.read_upgrade(stream))\n\n        return SpecialSkills(skills)\n\n    def write_upgrades(self, stream: core.Data):\n        for skill in self.skills:\n            skill.write_upgrade(stream)\n\n    def read_gatya_seen(self, stream: core.Data):\n        for skill in self.get_valid_skills():\n            skill.read_seen(stream)\n\n    def write_gatya_seen(self, stream: core.Data):\n        for skill in self.get_valid_skills():\n            skill.write_seen(stream)\n\n    def read_max_upgrade_levels(self, stream: core.Data):\n        for skill in self.skills:\n            skill.read_max_upgrade_level(stream)\n\n    def write_max_upgrade_levels(self, stream: core.Data):\n        for skill in self.skills:\n            skill.write_max_upgrade_level(stream)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [skill.serialize() for skill in self.skills]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> SpecialSkills:\n        skills = SpecialSkills([])\n        for skill in data:\n            skills.skills.append(SpecialSkill.deserialize(skill))\n\n        return skills\n\n    def __repr__(self) -> str:\n        return f\"Skills(skills={self.skills})\"\n\n    def __str__(self) -> str:\n        return f\"Skills(skills={self.skills})\"\n\n    def edit(self, save_file: core.SaveFile):\n        names_o = core.core_data.get_gatya_item_names(save_file)\n        items = core.core_data.get_gatya_item_buy(save_file).get_by_category(2)\n        if items is None:\n            return\n        names: list[str] = []\n        for item in items:\n            name = names_o.get_name(item.id)\n            if name is None:\n                return\n            names.append(name)\n        ids, _ = dialog_creator.ChoiceInput.from_reduced(\n            names, [], {}, \"special_skills_dialog\"\n        ).multiple_choice()\n        if not ids:\n            return\n        skills = self.get_valid_skills()\n        if len(ids) == 1:\n            option_id = 0\n        else:\n            options: list[str] = [\n                \"upgrade_individual_skill\",\n                \"upgrade_all_skills\",\n            ]\n            option_id = dialog_creator.ChoiceInput(\n                options, options, [], {}, \"upgrade_skills_select_mod\", True\n            ).single_choice()\n            if option_id is None:\n                return\n            option_id -= 1\n\n        ability_data = core.core_data.get_ability_data(save_file)\n        if ability_data.ability_data is None:\n            return\n        success = False\n        if option_id == 0:\n            for id in ids:\n                color.ColoredText.localize(\n                    \"selected_skill_upgrades\",\n                    name=names[id],\n                    base_level=skills[id].upgrade.base + 1,\n                    plus_level=skills[id].upgrade.plus,\n                )\n                ability = ability_data.get_ability_data_item(id)\n                if ability is None:\n                    continue\n                upgrade, should_exit = core.Upgrade.get_user_upgrade(\n                    ability.max_base_level - 1, ability.max_plus_level\n                )\n                if should_exit:\n                    return\n                if upgrade is not None:\n                    self.set_upgrade(id, upgrade)\n                    color.ColoredText.localize(\n                        \"selected_skill_upgraded\",\n                        name=names[id],\n                        base_level=skills[id].upgrade.base + 1,\n                        plus_level=skills[id].upgrade.plus,\n                    )\n                    success = True\n\n        elif option_id == 1:\n            max_base_level = max(\n                [ability.max_base_level for ability in ability_data.ability_data]\n            )\n            max_plus_level = max(\n                [ability.max_plus_level for ability in ability_data.ability_data]\n            )\n            upgrade, should_exit = core.Upgrade.get_user_upgrade(\n                max_base_level - 1, max_plus_level\n            )\n            if should_exit or upgrade is None:\n                return\n            disable_maxes = core.core_data.config.get_bool(core.ConfigKey.DISABLE_MAXES)\n            for id in ids:\n                max_base_level = ability_data.ability_data[id].max_base_level - 1\n                max_plus_level = ability_data.ability_data[id].max_plus_level\n                if disable_maxes:\n                    max_base_level = None\n                    max_plus_level = None\n\n                self.set_upgrade(\n                    id,\n                    upgrade.copy(),\n                    max_base=max_base_level,\n                    max_plus=max_plus_level,\n                )\n\n                color.ColoredText.localize(\n                    \"selected_skill_upgraded\",\n                    name=names[id],\n                    base_level=skills[id].upgrade.base + 1,\n                    plus_level=skills[id].upgrade.plus,\n                )\n            success = True\n\n        if success:\n            color.ColoredText.localize(\"skills_edited\")\n\n    def get_from_id(self, id: int, only_valid: bool = True) -> SpecialSkill | None:\n        if only_valid:\n            skills = self.get_valid_skills()\n        else:\n            skills = self.skills\n        if id >= len(skills) or id < 0:\n            return None\n        return skills[id]\n\n\nclass AbilityDataItem:\n    def __init__(\n        self,\n        index: int,\n        sell_price: int,\n        gatya_rarity: int,\n        max_base_level: int,\n        max_plus_level: int,\n        chapter_1_to_2_max_level: int,\n    ):\n        self.index = index\n        self.sell_price = sell_price\n        self.gatya_rarity = gatya_rarity\n        self.max_base_level = max_base_level\n        self.max_plus_level = max_plus_level\n        self.chapter_1_to_2_max_level = chapter_1_to_2_max_level\n\n\nclass AbilityData:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.ability_data = self.get_ability_data()\n\n    def get_ability_data(self) -> list[AbilityDataItem] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"AbilityData.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        ability_data: list[AbilityDataItem] = []\n        for i, row in enumerate(csv):\n            ability_data.append(\n                AbilityDataItem(\n                    i,\n                    row[0].to_int(),\n                    row[1].to_int(),\n                    row[2].to_int(),\n                    row[3].to_int(),\n                    row[4].to_int(),\n                )\n            )\n        return ability_data\n\n    def get_ability_data_item(self, item_id: int) -> AbilityDataItem | None:\n        if self.ability_data is None:\n            return None\n        return self.ability_data[item_id]\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/stamp.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass StampData:\n    def __init__(\n        self,\n        current_stamp: int,\n        collected_stamp: list[int],\n        unknown: int,\n        daily_reward: int,\n    ):\n        self.current_stamp = current_stamp\n        self.collected_stamp = collected_stamp\n        self.unknown = unknown\n        self.daily_reward = daily_reward\n\n    @staticmethod\n    def init() -> StampData:\n        return StampData(0, [0] * 30, 0, 0)\n\n    @staticmethod\n    def read(stream: core.Data) -> StampData:\n        current_stamp = stream.read_int()\n        collected_stamp = stream.read_int_list(30)\n        unknown = stream.read_int()\n        daily_reward = stream.read_int()\n        return StampData(current_stamp, collected_stamp, unknown, daily_reward)\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.current_stamp)\n        stream.write_int_list(self.collected_stamp, write_length=False)\n        stream.write_int(self.unknown)\n        stream.write_int(self.daily_reward)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"current_stamp\": self.current_stamp,\n            \"collected_stamp\": self.collected_stamp,\n            \"unknown\": self.unknown,\n            \"daily_reward\": self.daily_reward,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> StampData:\n        return StampData(\n            data.get(\"current_stamp\", 0),\n            data.get(\"collected_stamp\", []),\n            data.get(\"unknown\", 0),\n            data.get(\"daily_reward\", 0),\n        )\n\n    def __repr__(self):\n        return f\"StampData({self.current_stamp}, {self.collected_stamp}, {self.unknown}, {self.daily_reward})\"\n\n    def __str__(self):\n        return f\"StampData({self.current_stamp}, {self.collected_stamp}, {self.unknown}, {self.daily_reward})\"\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/talent_orbs.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass TalentOrb:\n    def __init__(self, id: int, value: int):\n        self.id = id\n        self.value = value\n\n    @staticmethod\n    def init() -> TalentOrb:\n        return TalentOrb(\n            0,\n            0,\n        )\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> TalentOrb:\n        id = stream.read_short()\n        if gv < 110400:\n            value = stream.read_byte()\n        else:\n            value = stream.read_short()\n        return TalentOrb(id, value)\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        stream.write_short(self.id)\n        if gv < 110400:\n            stream.write_byte(self.value)\n        else:\n            stream.write_short(self.value)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"id\": self.id,\n            \"value\": self.value,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> TalentOrb:\n        return TalentOrb(data.get(\"id\", 0), data.get(\"value\", 0))\n\n    def __repr__(self):\n        return f\"Orb({self.id}, {self.value})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass TalentOrbs:\n    def __init__(self, orbs: dict[int, TalentOrb]):\n        self.orbs = orbs\n\n    @staticmethod\n    def init() -> TalentOrbs:\n        return TalentOrbs({})\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> TalentOrbs:\n        length = stream.read_short()\n        orbs: dict[int, TalentOrb] = {}\n        for _ in range(length):\n            orb = TalentOrb.read(stream, gv)\n            orbs[orb.id] = orb\n        return TalentOrbs(orbs)\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        stream.write_short(len(self.orbs))\n        for orb in self.orbs.values():\n            orb.write(stream, gv)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [orb.serialize() for orb in self.orbs.values()]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> TalentOrbs:\n        return TalentOrbs(\n            {orb.get(\"id\", 0): TalentOrb.deserialize(orb) for orb in data}\n        )\n\n    def __repr__(self):\n        return f\"TalentOrbs({self.orbs})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def set_orb(self, id: int, value: int):\n        self.orbs[id] = TalentOrb(id, value)\n\n\nclass RawOrbInfo:\n    def __init__(\n        self,\n        orb_id: int,\n        rank_id: int,\n        effect_id: int,\n        value: list[int],\n        target_id: int | None,\n    ):\n        self.orb_id = orb_id\n        self.rank_id = rank_id\n        self.effect_id = effect_id\n        self.value = value\n        self.target_id = target_id\n\n\nclass OrbInfo:\n    def __init__(\n        self,\n        raw_orb_info: RawOrbInfo,\n        rank: str,\n        target: str | None,\n        effect: str,\n    ):\n        self.raw_orb_info = raw_orb_info\n        self.rank = rank\n        self.target = target\n        self.effect = effect\n\n    def __str__(self) -> str:\n        \"\"\"Get the string representation of the OrbInfo\n\n        Returns:\n            str: The string representation of the OrbInfo\n        \"\"\"\n        target_color = color_from_enemy_type(self.raw_orb_info.target_id)\n        rank_color = color_from_grade(self.raw_orb_info.rank_id)\n        effect_color = color_from_effect(self.raw_orb_info.effect_id)\n        effect_text = self.effect.replace(\"%@\", \"{}\")\n        effect_text = f\"<{effect_color}>{effect_text}</>\"\n        target = self.target\n        effect = effect_text.format(\n            f\"<{rank_color}>{self.rank}</>\",\n            f\"<{target_color}>{target}</>\" if target else \"\",\n        )\n        return f\"{effect}\"\n\n    def to_colortext(self) -> str:\n        \"\"\"Get the string representation of the OrbInfo with color\n\n        Returns:\n            str: The string representation of the OrbInfo with color\n        \"\"\"\n        return str(self)\n\n    @staticmethod\n    def create_unknown(orb_id: int) -> OrbInfo:\n        \"\"\"Create an unknown OrbInfo\n\n        Args:\n            orb_id (int): The id of the orb\n\n        Returns:\n            OrbInfo: The unknown OrbInfo\n        \"\"\"\n        return OrbInfo(\n            RawOrbInfo(orb_id, 0, 0, [], 0),\n            \"???\",\n            \"\",\n            \"%@:%@\",\n        )\n\n\nclass OrbInfoList:\n    equipment_data_file_name = \"DataLocal/equipmentlist.json\"\n    grade_list_file_name = \"DataLocal/equipmentgrade.csv\"\n    attribute_list_file_name = \"resLocal/attribute_explonation.tsv\"\n    effect_list_file_name = \"resLocal/equipment_explonation.tsv\"\n\n    def __init__(self, orb_info_list: list[OrbInfo]):\n        \"\"\"Initialize the OrbInfoList class\n\n        Args:\n            orb_info_list (list[OrbInfo]): The list of OrbInfo\n        \"\"\"\n        self.orb_info_list = orb_info_list\n\n    @staticmethod\n    def create(save_file: core.SaveFile) -> OrbInfoList | None:\n        \"\"\"Create an OrbInfoList\n\n        Args:\n            save_file (core.SaveFile): The save file\n\n        Returns:\n            OrbInfoList | None: The OrbInfoList\n        \"\"\"\n        gdg = core.core_data.get_game_data_getter(save_file)\n        json_data_file = gdg.download_from_path(OrbInfoList.equipment_data_file_name)\n        grade_list_file = gdg.download_from_path(OrbInfoList.grade_list_file_name)\n        attribute_list_file = gdg.download_from_path(\n            OrbInfoList.attribute_list_file_name\n        )\n        equipment_list_file = gdg.download_from_path(OrbInfoList.effect_list_file_name)\n        if (\n            json_data_file is None\n            or grade_list_file is None\n            or attribute_list_file is None\n            or equipment_list_file is None\n        ):\n            return None\n        raw_orbs = OrbInfoList.parse_json_data(json_data_file)\n        if raw_orbs is None:\n            return None\n        orbs = OrbInfoList.load_names(\n            raw_orbs, grade_list_file, attribute_list_file, equipment_list_file\n        )\n        return OrbInfoList(orbs)\n\n    @staticmethod\n    def parse_json_data(json_data: core.Data) -> list[RawOrbInfo] | None:\n        \"\"\"Parse the json data of the equipment\n\n        Args:\n            json_data (core.Data): The json data\n\n        Returns:\n            list[RawOrbInfo]: The list of RawOrbInfo\n        \"\"\"\n        try:\n            data: dict[str, Any] = core.JsonFile.from_data(json_data).to_object()\n        except core.JSONDecodeError:\n            return None\n        orb_info_list: list[RawOrbInfo] = []\n        for id, orb in enumerate(data[\"ID\"]):\n            grade_id = orb[\"gradeID\"]\n            content = orb[\"content\"]\n            value = orb[\"value\"]\n            attribute = orb.get(\"attribute\")\n            orb_info_list.append(RawOrbInfo(id, grade_id, content, value, attribute))\n        return orb_info_list\n\n    @staticmethod\n    def load_names(\n        raw_orb_info: list[RawOrbInfo],\n        grade_data: core.Data,\n        attribute_data: core.Data,\n        effect_data: core.Data,\n    ) -> list[OrbInfo]:\n        \"\"\"Load the names of the equipment\n\n        Args:\n            raw_orb_info (list[RawOrbInfo]): The list of RawOrbInfo\n            grade_data (core.Data): Raw data of the grade list\n            attribute_data (core.Data): Raw data of the attribute list\n            effect_data (core.Data): Raw data of the effect list\n\n        Returns:\n            list[OrbInfo]: The list of OrbInfo\n        \"\"\"\n        grade_csv = core.CSV(grade_data)\n        attribute_tsv = core.CSV(attribute_data, \"\\t\")\n        effect_csv = core.CSV(effect_data, \"\\t\")\n        orb_info_list: list[OrbInfo] = []\n        for orb in raw_orb_info:\n            grade = grade_csv[orb.rank_id][3].to_str()\n            effect = effect_csv[orb.effect_id][0].to_str()\n\n            if orb.target_id is not None:\n                attribute = attribute_tsv[orb.target_id][0].to_str()\n            else:\n                attribute = None\n\n            orb_info_list.append(OrbInfo(orb, grade, attribute, effect))\n        return orb_info_list\n\n    def get_orb_info(self, orb_id: int) -> OrbInfo | None:\n        \"\"\"Get the OrbInfo from the id\n\n        Args:\n            orb_id (int): The id of the orb\n\n        Returns:\n            OrbInfo | None: The OrbInfo\n        \"\"\"\n        try:\n            return self.orb_info_list[orb_id]\n        except IndexError:\n            return None\n\n    def get_orb_from_components(\n        self,\n        grade: str,\n        attribute: str | None,\n        effect: str,\n    ) -> OrbInfo | None:\n        \"\"\"Get the OrbInfo from the components\n\n        Args:\n            grade (str): The grade of the orb\n            attribute (str | None): The attribute of the orb. None if applies to all attributes\n            effect (str): The effect of the orb\n\n        Returns:\n            OrbInfo | None: The OrbInfo\n        \"\"\"\n        for orb in self.orb_info_list:\n            if orb.rank == grade and orb.target == attribute and orb.effect == effect:\n                return orb\n        return None\n\n    def does_match_orb_str(self, str_1: str | None, str_2: str | None) -> bool:\n        if str_2 == \"*\":\n            return True\n\n        if str_1 is None:\n            return str_2 is None\n        if str_2 is None:\n            return False\n\n        return str_1.lower() == str_2.lower()\n\n    def get_orbs_from_component_fuzzy(\n        self,\n        grade: str,\n        attribute: str | None,\n        effect: str,\n    ) -> list[OrbInfo]:\n        \"\"\"Get the OrbInfo from the components matching the first word of the effect and lowercased\n\n        Args:\n            grade (str): The grade of the orb\n            attribute (str | None): The attribute of the orb. None if all\n            effect (str): The effect of the orb\n\n        Returns:\n            list[OrbInfo]: The list of OrbInfo\n        \"\"\"\n        orbs: list[OrbInfo] = []\n        for orb in self.orb_info_list:\n            if (\n                (orb.rank.lower() == grade.lower() or grade == \"*\")\n                and (self.does_match_orb_str(orb.target, attribute))\n                and (orb.effect == effect or effect == \"*\")\n            ):\n                orbs.append(orb)\n        return orbs\n\n    def get_all_grades(self) -> list[str]:\n        \"\"\"Get all the grades\n\n        Returns:\n            list[str]: The list of grades\n        \"\"\"\n\n        data = list(\n            set([(orb.rank, orb.raw_orb_info.rank_id) for orb in self.orb_info_list])\n        )\n\n        data.sort(key=lambda id: id[1])\n\n        return [orb[0] for orb in data]\n\n    def get_all_attributes(self) -> list[str | None]:\n        \"\"\"Get all the attributes\n\n        Returns:\n            list[str]: The list of attributes\n        \"\"\"\n\n        data = list(\n            set(\n                [\n                    (orb.target, orb.raw_orb_info.target_id)\n                    for orb in self.orb_info_list\n                    if orb.target is not None and orb.raw_orb_info.target_id is not None\n                ]\n            )\n        )\n\n        data.sort(key=lambda id: id[1])\n\n        return [orb[0] for orb in data]\n\n    def get_all_effects(self) -> list[str]:\n        \"\"\"Get all the effects\n\n        Returns:\n            list[str]: The list of effects\n        \"\"\"\n\n        data = list(\n            set(\n                [(orb.effect, orb.raw_orb_info.effect_id) for orb in self.orb_info_list]\n            )\n        )\n\n        data.sort(key=lambda id: id[1])\n\n        return [orb[0] for orb in data]\n\n\nclass SaveOrb:\n    \"\"\"Represents a saved orb in the save file\"\"\"\n\n    def __init__(self, orb: OrbInfo, count: int):\n        \"\"\"Initialize the SaveOrb class\n\n        Args:\n            orb (OrbInfo): The OrbInfo\n            count (int): The amount of the orb\n        \"\"\"\n        self.count = count\n        self.orb = orb\n\n\ndef color_from_enemy_type(target_id: int | None) -> str:\n    if target_id is None:\n        return color.ColorHex.WHITE\n    if target_id == 0:\n        return color.ColorHex.RED\n    elif target_id == 1:\n        return color.ColorHex.GREEN\n    elif target_id == 2:\n        return color.ColorHex.DARK_GREY\n    elif target_id == 3:\n        return color.ColorHex.LIGHT_GREY\n    elif target_id == 4:\n        return color.ColorHex.YELLOW\n    elif target_id == 5:\n        return color.ColorHex.BLUE\n    elif target_id == 6:\n        return color.ColorHex.MAGENTA\n    elif target_id == 7:\n        return color.ColorHex.DARK_GREEN\n    elif target_id == 8:\n        return color.ColorHex.WHITE\n    elif target_id == 9:\n        return color.ColorHex.DARK_MAGENTA\n    elif target_id == 10:\n        return color.ColorHex.ORANGE\n    elif target_id == 11:\n        return color.ColorHex.CYAN\n    return color.ColorHex.BLACK\n\n\ndef color_from_grade(grade_id: int) -> str:\n    if grade_id == 0:\n        return color.ColorHex.RED\n    elif grade_id == 1:\n        return color.ColorHex.ORANGE\n    elif grade_id == 2:\n        return color.ColorHex.YELLOW\n    elif grade_id == 3:\n        return color.ColorHex.GREEN\n    elif grade_id == 4:\n        return color.ColorHex.BLUE\n    return color.ColorHex.BLACK\n\n\ndef color_from_effect(effect_id: int):\n    # if effect_id == 0:\n    #     return color.ColorHex.RED\n    # elif effect_id == 1:\n    #     return color.ColorHex.GREEN\n    # elif effect_id == 2:\n    #     return color.ColorHex.DARK_GREY\n    # elif effect_id == 3:\n    #     return color.ColorHex.LIGHT_GREY\n    # elif effect_id == 4:\n    #     return color.ColorHex.YELLOW\n    # elif effect_id == 5:\n    #     return color.ColorHex.BLUE\n    # elif effect_id == 6:\n    #     return color.ColorHex.MAGENTA\n    # elif effect_id == 7:\n    #     return color.ColorHex.DARK_GREEN\n    # elif effect_id == 8:\n    #     return color.ColorHex.WHITE\n    # elif effect_id == 9:\n    #     return color.ColorHex.DARK_MAGENTA\n    # elif effect_id == 10:\n    #     return color.ColorHex.ORANGE\n\n    return \"@t\"\n\n\nclass SaveOrbs:\n    def __init__(\n        self,\n        orbs: dict[int, SaveOrb],\n        orb_info_list: OrbInfoList,\n    ):\n        \"\"\"Initialize the SaveOrbs class\n\n        Args:\n            orbs (dict[int, SaveOrb]): The orbs\n            orb_info_list (OrbInfoList): The orb info list\n        \"\"\"\n        self.orbs = orbs\n        self.orb_info_list = orb_info_list\n\n    @staticmethod\n    def from_save_file(save_file: core.SaveFile) -> SaveOrbs | None:\n        \"\"\"Create a SaveOrbs from the save stats\n\n        Args:\n            save_file (core.SaveFile): The save file\n\n        Returns:\n            SaveOrbs | None: The SaveOrbs\n        \"\"\"\n        orb_info_list = OrbInfoList.create(save_file)\n        if orb_info_list is None:\n            return None\n        orbs: dict[int, SaveOrb] = {}\n        for orb_id, orb in save_file.talent_orbs.orbs.items():\n            try:\n                orb_info = orb_info_list.orb_info_list[int(orb_id)]\n            except IndexError:\n                orb_info = OrbInfo.create_unknown(int(orb_id))\n            orbs[int(orb_id)] = SaveOrb(orb_info, orb.value)\n\n        return SaveOrbs(orbs, orb_info_list)\n\n    def print(self):\n        \"\"\"Print the orbs as a formatted list\"\"\"\n        self.sort_orbs()\n        total_orbs = sum([orb.count for orb in self.orbs.values()])\n        color.ColoredText.localize(\"total_current_orbs\", total_orbs=total_orbs)\n        color.ColoredText.localize(\n            \"total_current_orb_types\", total_types=len(self.orbs)\n        )\n        color.ColoredText.localize(\"current_orbs\")\n        for orb in self.orbs.values():\n            color.ColoredText(f\"<@q>{orb.count}</> {orb.orb.to_colortext()}\")\n\n    def sort_orbs(self):\n        \"\"\"Sort the orbs by attribute, effect, grade and id in that order with attribute being the most important\"\"\"\n        orbs = list(self.orbs.values())\n        orbs.sort(key=lambda orb: orb.orb.raw_orb_info.orb_id)\n        orbs.sort(key=lambda orb: orb.orb.raw_orb_info.rank_id)\n        orbs.sort(key=lambda orb: orb.orb.raw_orb_info.effect_id)\n        orbs.sort(key=lambda orb: orb.orb.raw_orb_info.target_id or -1)\n\n    def localize_attribute(self, attribute: str | None) -> str | None:\n        if attribute is not None:\n            return attribute\n\n    def edit(self):\n        \"\"\"Edit the orbs\"\"\"\n        # this code sucks quit a lot, but it works and i can't be bothered making it better atm\n        self.print()\n        all_grades = self.orb_info_list.get_all_grades()\n        all_grades = [grade for grade in all_grades]\n        all_grades.sort()\n        all_attributes = self.orb_info_list.get_all_attributes()\n        all_attributes = [\n            self.localize_attribute(attribute) or \"\"\n            for attribute in all_attributes\n            if attribute\n        ]\n        all_attributes.sort()\n        all_effects = self.orb_info_list.get_all_effects()\n        all_effects.sort()\n        all_effects_str = [\n            effect.lower().replace(\"%@\", \"\").replace(\":\", \"\").strip() + f\" <@s>({i})</>\"\n            for (i, effect) in enumerate(all_effects)\n        ]\n        all_effect_ids = [i for i in range(len(all_effects))]\n\n        all_grades_str = \",\".join(\n            f\"<{color_from_grade(self.orb_info_list.get_all_grades().index(grade))}>{grade}</>\"\n            for grade in all_grades\n        )\n\n        all_attributes_str = \",\".join(\n            f\"<{color_from_enemy_type(self.orb_info_list.get_all_attributes().index(attribute))}>{attribute}</>\"\n            for attribute in all_attributes\n        )\n\n        all_effects_str = \", \".join(\n            f\"<{color_from_effect(self.orb_info_list.get_all_effects().index(effect))}>{effect_str}</>\"\n            for effect_str, effect in zip(all_effects_str, all_effects)\n        )\n\n        color.ColoredText.localize(\n            \"edit_orbs_help\",\n            escape=False,\n            all_grades_str=all_grades_str,\n            all_attributes_str=all_attributes_str,\n            all_effects_str=all_effects_str,\n        )\n\n        orb_input_selection = (\n            color.ColoredInput()\n            .localize(\"orb_select\")\n            .lower()\n            .replace(\"angle\", \"angel\")\n            .split(\",\")\n        )\n        if orb_input_selection == [core.core_data.local_manager.get_key(\"quit_key\")]:\n            return\n\n        orb_selection: list[OrbInfo] = []\n\n        for orb_input in orb_input_selection:\n            grade = None\n            attribute = None\n            effect = None\n            orb_input = orb_input.strip()\n            parts = orb_input.split(\" \")\n            parts = [part.lower() for part in parts if part != \"\"]\n            if len(parts) == 0:\n                continue\n            if parts[0] == \"*\":\n                orb_selection = self.orb_info_list.orb_info_list\n                break\n            for available_grade in all_grades:\n                if available_grade.lower() in parts:\n                    grade = available_grade\n                    break\n            for available_attribute in all_attributes:\n                if available_attribute.lower() in parts:\n                    attribute = available_attribute\n                    break\n            for available_effect in all_effect_ids:\n                if str(available_effect) in parts:\n                    effect = all_effects[available_effect]\n                    break\n            if grade is None:\n                grade = \"*\"\n            if attribute is None:\n                attribute = \"*\"\n            if effect is None:\n                effect = \"*\"\n            orbs = self.orb_info_list.get_orbs_from_component_fuzzy(\n                grade, attribute, effect\n            )\n            orb_selection.extend(orbs)\n\n        orb_selection = list(set(orb_selection))\n        orb_selection.sort(key=lambda orb: orb.raw_orb_info.orb_id)\n        orb_selection.sort(key=lambda orb: orb.raw_orb_info.rank_id)\n        orb_selection.sort(key=lambda orb: orb.raw_orb_info.effect_id)\n        orb_selection.sort(key=lambda orb: orb.raw_orb_info.target_id or -1)\n\n        color.ColoredText.localize(\"selected_orbs\")\n\n        for orb in orb_selection:\n            color.ColoredText(orb.to_colortext())\n\n        max_orbs = core.core_data.max_value_manager.get(\"talent_orbs\")\n\n        if len(orb_selection) == 0:\n            return\n        if len(orb_selection) == 1:\n            individual = True\n        else:\n            individual = dialog_creator.ChoiceInput.from_reduced(\n                [\"individual\", \"edit_all_at_once\"],\n                dialog=\"edit_orbs_individually\",\n                single_choice=True,\n            ).single_choice()\n            if individual is None:\n                return\n            individual = True if individual == 1 else False\n        if individual:\n            for orb in orb_selection:\n                orb_id = orb.raw_orb_info.orb_id\n                try:\n                    orb_count = self.orbs[orb_id].count\n                except KeyError:\n                    orb_count = 0\n\n                orb_count = dialog_creator.SingleEditor(\n                    orb.to_colortext(), orb_count, max_orbs\n                ).edit(escape_text=False)\n\n                self.orbs[orb_id] = SaveOrb(orb, orb_count)\n\n        else:\n            int_input = dialog_creator.IntInput(max_orbs)\n            orb_count = int_input.get_input_locale_while(\n                \"edit_orbs_all\", {\"max\": max_orbs}, escape=False\n            )\n            if orb_count is None:\n                return\n            orb_count = int_input.clamp_value(orb_count)\n            for orb in orb_selection:\n                orb_id = orb.raw_orb_info.orb_id\n                self.orbs[orb_id] = SaveOrb(orb, orb_count)\n\n        self.print()\n\n    def save(self, save_file: core.SaveFile):\n        \"\"\"Save the orbs to the save_stats\n\n        Args:\n            save_file (core.SaveFile): The save_stats to save the orbs to\n        \"\"\"\n        for orb_id, orb in self.orbs.items():\n            save_file.talent_orbs.orbs[orb_id] = core.TalentOrb(orb_id, orb.count)\n\n    @staticmethod\n    def edit_talent_orbs(save_file: core.SaveFile):\n        \"\"\"Edit the talent orbs\n\n        Args:\n            save_file (core.SaveFile): The save_stats to edit the orbs of\n\n        \"\"\"\n        save_orbs = SaveOrbs.from_save_file(save_file)\n        if save_orbs is None:\n            color.ColoredText.localize(\"failed_to_load_orbs\")\n            return None\n        save_orbs.edit()\n        save_orbs.save(save_file)\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/unlock_popups.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\n\nclass Popup:\n    def __init__(self, seen: bool):\n        self.seen = seen\n\n    @staticmethod\n    def init() -> Popup:\n        return Popup(False)\n\n    @staticmethod\n    def read(stream: core.Data) -> Popup:\n        seen = stream.read_bool()\n        return Popup(seen)\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.seen)\n\n    def serialize(self) -> bool:\n        return self.seen\n\n    @staticmethod\n    def deserialize(data: bool) -> Popup:\n        return Popup(data)\n\n    def __repr__(self) -> str:\n        return f\"Popup(seen={self.seen!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass UnlockPopups:\n    def __init__(self, popups: dict[int, Popup]):\n        self.popups = popups\n\n    @staticmethod\n    def init() -> UnlockPopups:\n        return UnlockPopups({})\n\n    @staticmethod\n    def read(stream: core.Data) -> UnlockPopups:\n        total = stream.read_int()\n        popups: dict[int, Popup] = {}\n        for _ in range(total):\n            key = stream.read_int()\n            popups[key] = Popup.read(stream)\n        return UnlockPopups(popups)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.popups))\n        for key, popup in self.popups.items():\n            stream.write_int(key)\n            popup.write(stream)\n\n    def serialize(self) -> dict[int, bool]:\n        return {key: popup.serialize() for key, popup in self.popups.items()}\n\n    @staticmethod\n    def deserialize(data: dict[int, bool]) -> UnlockPopups:\n        return UnlockPopups(\n            {int(key): Popup.deserialize(popup) for key, popup in data.items()}\n        )\n\n    def __repr__(self) -> str:\n        return f\"Popups(popups={self.popups!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass UnlockPopupLine:\n    def __init__(\n        self,\n        popup_id: int,\n        enabled: bool,\n        conditions: int,\n        stage: int,\n        map_conditions: int,\n        user_rank: int,\n        get_char_id1: int,\n        get_char_id2: int,\n        os_id: int,\n        unlock_eye_1_id: int,\n        add_level1: int,\n        unlock_eye_2_id: int,\n        add_level2: int,\n        unlock_plus_id: int,\n        add_level: int,\n        skill_id: int,\n        item_id: int,\n        num: int,\n        help_enabled: bool,\n    ):\n        self.popup_id = popup_id\n        self.enabled = enabled\n        self.conditions = conditions\n        self.stage = stage\n        self.map_conditions = map_conditions\n        self.user_rank = user_rank\n        self.get_char_id1 = get_char_id1\n        self.get_char_id2 = get_char_id2\n        self.os_id = os_id\n        self.unlock_eye_1_id = unlock_eye_1_id\n        self.add_level1 = add_level1\n        self.unlock_eye_2_id = unlock_eye_2_id\n        self.add_level2 = add_level2\n        self.unlock_plus_id = unlock_plus_id\n        self.add_level = add_level\n        self.skill_id = skill_id\n        self.item_id = item_id\n        self.num = num\n        self.help_enabled = help_enabled\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> UnlockPopupLine:\n        return UnlockPopupLine(\n            row.next_int(),\n            row.next_bool(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_int(),\n            row.next_bool(),\n        )\n\n\nclass UnlockPopupData:\n    def __init__(self, popups: list[UnlockPopupLine]):\n        self.popups = popups\n\n    @staticmethod\n    def from_csv(csv: core.CSV) -> UnlockPopupData:\n        popups: list[UnlockPopupLine] = []\n        for line in csv.lines[1:]:\n            popups.append(UnlockPopupLine.from_csv_row(line))\n\n        return UnlockPopupData(popups)\n\n    @staticmethod\n    def from_save(save_file: core.SaveFile) -> UnlockPopupData | None:\n        gdg = core.core_data.get_game_data_getter(save_file)\n        data = gdg.download(\"DataLocal\", \"unlockPopup.tsv\")\n        if data is None:\n            return None\n\n        csv = core.CSV(data, \"\\t\")\n\n        return UnlockPopupData.from_csv(csv)\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/upgrade.py",
    "content": "from __future__ import annotations\nimport random\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\nclass Upgrade:\n    def __init__(self, plus: int, base: int):\n        self.plus = plus\n        self.base = base\n\n        self.base_range = None\n        self.plus_range = None\n\n    def get_base(self) -> int:\n        return self.base + 1\n\n    def get_total(self) -> int:\n        return self.get_base() + self.get_plus()\n\n    def get_plus(self) -> int:\n        return self.plus\n\n    def upgrade(self):\n        self.base += 1\n\n    def increment_base(self, amount: int):\n        self.base += amount\n\n    def increment_plus(self, amount: int):\n        self.plus += amount\n\n    def get_random_base(self, max_base: int | None = None) -> int:\n        if self.base_range is None:\n            return self.base\n        base = random.randint(self.base_range[0], self.base_range[1])\n        if max_base is not None:\n            base = min(base, max_base)\n        return base\n\n    def get_random_plus(self, max_plus: int | None = None) -> int:\n        if self.plus_range is None:\n            return self.plus\n        plus = random.randint(self.plus_range[0], self.plus_range[1])\n        if max_plus is not None:\n            plus = min(plus, max_plus)\n        return plus\n\n    @staticmethod\n    def read(stream: core.Data) -> Upgrade:\n        plus = stream.read_ushort()\n        base = stream.read_ushort()\n\n        return Upgrade(plus, base)\n\n    def write(self, stream: core.Data):\n        stream.write_ushort(self.plus)\n        stream.write_ushort(self.base)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"plus\": self.plus,\n            \"base\": self.base,\n        }\n\n    @staticmethod\n    def init() -> Upgrade:\n        return Upgrade(0, 0)\n\n    def reset(self):\n        self.plus = 0\n        self.base = 0\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Upgrade:\n        return Upgrade(data.get(\"plus\", 0), data.get(\"base\", 0))\n\n    def __repr__(self) -> str:\n        return f\"Upgrade(plus={self.plus}, base={self.base})\"\n\n    def __str__(self) -> str:\n        return f\"Upgrade(plus={self.plus}, base={self.base})\"\n\n    @staticmethod\n    def get_user_upgrade(\n        max_pos_base: int,\n        max_pos_plus: int,\n    ) -> tuple[Upgrade | None, bool]:\n        disable_maxes = core.core_data.config.get_bool(core.ConfigKey.DISABLE_MAXES)\n        if disable_maxes:\n            max_pos_base = 50_000\n            max_pos_plus = 50_000\n        color.ColoredText.localize(\n            \"max_upgrade\", max_base=max_pos_base + 1, max_plus=max_pos_plus\n        )\n        usr_input = color.ColoredInput().localize(\"upgrade_input\")\n        if usr_input == core.core_data.local_manager.get_key(\"quit_key\"):\n            return None, True\n        # example:\n        # 10+20 = Upgrade(base=9, plus=20)\n        # 10+ = Upgrade(base=9, plus=-1) # -1 means no change\n        # +20 = Upgrade(base=-1, plus=20) # -1 means no change\n        # 10 = Upgrade(base=9, plus=0)\n        # 5-10+20-30 = Upgrade(base=random.randint(4, 9), plus=random.randint(20, 30))\n        # 5-10+ = Upgrade(base=random.randint(4, 9), plus=-1)\n        # +20-30 = Upgrade(base=-1, plus=random.randint(20, 30))\n        # max+max = Upgrade(base=50000, plus=50000)\n\n        parts = usr_input.split(\"+\")\n        if len(parts) == 1:\n            base = parts[0]\n            plus = \"0\"\n        else:\n            base = parts[0]\n            plus = parts[1]\n\n        min_base, max_base = None, None\n        min_plus, max_plus = None, None\n\n        max_text = core.core_data.local_manager.get_key(\"max\")\n\n        if not base:\n            base_int = -1\n        else:\n            range_parts = base.split(\"-\")\n            if len(range_parts) == 1:\n                if range_parts[0].strip() == max_text:\n                    min_base = max_pos_base\n                    max_base = max_pos_base\n                else:\n                    try:\n                        min_base = int(range_parts[0]) - 1\n                        max_base = min_base\n                    except ValueError:\n                        color.ColoredText.localize(\"invalid_upgrade_base\", base=base)\n                        return None, False\n            else:\n                try:\n                    min_base = int(range_parts[0]) - 1\n                    max_base = int(range_parts[1]) - 1\n                except ValueError:\n                    color.ColoredText.localize(\n                        \"invalid_upgrade_base_random\",\n                        min=range_parts[0],\n                        max=range_parts[1],\n                    )\n                    return None, False\n\n            base_int = (min_base + max_base) // 2\n\n        if not plus:\n            plus_int = -1\n        else:\n            range_parts = plus.split(\"-\")\n            if len(range_parts) == 1:\n                if range_parts[0].strip() == max_text:\n                    min_plus = max_pos_plus\n                    max_plus = max_pos_plus\n                else:\n                    try:\n                        min_plus = int(range_parts[0])\n                        max_plus = min_plus\n                    except ValueError:\n                        color.ColoredText.localize(\"invalid_upgrade_plus\", plus=plus)\n                        return None, False\n            else:\n                try:\n                    min_plus = int(range_parts[0])\n                    max_plus = int(range_parts[1])\n                except ValueError:\n                    color.ColoredText.localize(\n                        \"invalid_upgrade_plus_random\",\n                        min=range_parts[0],\n                        max=range_parts[1],\n                    )\n                    return None, False\n\n            plus_int = (min_plus + max_plus) // 2\n\n        upgrade = Upgrade(plus_int, base_int)\n        upgrade.base_range = (\n            max(0, min(min_base or base_int, max_pos_base)),\n            max(0, min(max_base or base_int, max_pos_base)),\n        )\n        upgrade.plus_range = (\n            max(0, min(min_plus or plus_int, max_pos_plus)),\n            max(0, min(max_plus or plus_int, max_pos_plus)),\n        )\n        return upgrade, False\n\n    def copy(self) -> Upgrade:\n        upgrade = Upgrade(self.plus, self.base)\n        upgrade.base_range = self.base_range\n        upgrade.plus_range = self.plus_range\n        return upgrade\n"
  },
  {
    "path": "src/bcsfe/core/game/catbase/user_rank_rewards.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass RankGift:\n    def __init__(\n        self, index: int, threshold: int, rewards: list[tuple[int, int]]\n    ):\n        self.index = index\n        self.threshold = threshold\n        self.rewards = rewards\n\n    def get_name(\n        self, rank_gift_descriptions: RankGiftDescriptions\n    ) -> str | None:\n        return rank_gift_descriptions.get_name(self.threshold)\n\n\nclass RankGifts:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.rank_gift = self.read_rank_gift()\n\n    def read_rank_gift(self) -> list[RankGift] | None:\n        rank_gift: list[RankGift] = []\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"rankGift.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        for i, line in enumerate(csv):\n            rewards: list[tuple[int, int]] = []\n            for col in range(1, len(line), 2):\n                value = line[col].to_int()\n                if value == -1:\n                    break\n                rewards.append((value, line[col + 1].to_int()))\n            rank_gift.append(RankGift(i, line[0].to_int(), rewards))\n        return rank_gift\n\n    def get_rank_gift(self, user_rank: int) -> RankGift | None:\n        if self.rank_gift is None:\n            return None\n        for rank_gift in self.rank_gift:\n            if rank_gift.threshold == user_rank:\n                return rank_gift\n        return None\n\n    def get_all_rank_gifts(self, user_rank: int) -> list[RankGift] | None:\n        if self.rank_gift is None:\n            return None\n        return [\n            rank_gift\n            for rank_gift in self.rank_gift\n            if rank_gift.threshold <= user_rank\n        ]\n\n    def get_by_id(self, id: int) -> RankGift | None:\n        if self.rank_gift is None:\n            return None\n        if id >= len(self.rank_gift) or id < 0:\n            return None\n        return self.rank_gift[id]\n\n    def get_all_unlocked(self, user_rank: int) -> list[RankGift] | None:\n        if self.rank_gift is None:\n            return None\n\n        return [\n            rank_gift\n            for rank_gift in self.rank_gift\n            if rank_gift.threshold <= user_rank\n        ]\n\n\nclass RankGiftDescription:\n    def __init__(self, index: int, threshold: int, description: str):\n        self.index = index\n        self.threshold = threshold\n        self.description = description\n\n\nclass RankGiftDescriptions:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.rank_gift_descriptions = self.read_rank_gift_descriptions()\n\n    def read_rank_gift_descriptions(self) -> list[RankGiftDescription] | None:\n        rank_gift_descriptions: list[RankGiftDescription] = []\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"resLocal\", \"user_info.tsv\")\n        if data is None:\n            return None\n        csv = core.CSV(data, delimiter=\"\\t\")\n        for i, line in enumerate(csv):\n            rank_gift_descriptions.append(\n                RankGiftDescription(i, line[0].to_int(), line[1].to_str())\n            )\n        return rank_gift_descriptions\n\n    def get_name(self, user_rank: int) -> str | None:\n        if self.rank_gift_descriptions is None:\n            return None\n        for rank_gift_description in self.rank_gift_descriptions:\n            if rank_gift_description.threshold == user_rank:\n                return rank_gift_description.description\n        return None\n\n\nclass Reward:\n    def __init__(self, claimed: bool):\n        self.claimed = claimed\n\n    @staticmethod\n    def init() -> Reward:\n        return Reward(False)\n\n    @staticmethod\n    def read(stream: core.Data) -> Reward:\n        return Reward(stream.read_bool())\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.claimed)\n\n    def serialize(self) -> bool:\n        return self.claimed\n\n    @staticmethod\n    def deserialize(data: bool) -> Reward:\n        return Reward(data)\n\n    def __repr__(self) -> str:\n        return f\"Reward(claimed={self.claimed})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass UserRankRewards:\n    def __init__(self, rewards: list[Reward]):\n        self.rewards = rewards\n        self.rank_gifts: RankGifts | None = None\n\n    def read_rank_gifts(self, save_file: core.SaveFile) -> RankGifts:\n        if self.rank_gifts is None:\n            self.rank_gifts = RankGifts(save_file)\n        return self.rank_gifts\n\n    @staticmethod\n    def init(gv: core.GameVersion) -> UserRankRewards:\n        if gv >= 30:\n            total = 0\n        else:\n            total = 50\n        rewards = [Reward.init() for _ in range(total)]\n        return UserRankRewards(rewards)\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> UserRankRewards:\n        if gv >= 30:\n            total = stream.read_int()\n        else:\n            total = 50\n        rewards: list[Reward] = []\n        for _ in range(total):\n            rewards.append(Reward.read(stream))\n        return UserRankRewards(rewards)\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        if gv >= 30:\n            stream.write_int(len(self.rewards))\n        for reward in self.rewards:\n            reward.write(stream)\n\n    def serialize(self) -> list[bool]:\n        return [reward.serialize() for reward in self.rewards]\n\n    @staticmethod\n    def deserialize(data: list[bool]) -> UserRankRewards:\n        return UserRankRewards([Reward.deserialize(reward) for reward in data])\n\n    def __repr__(self) -> str:\n        return f\"Rewards(rewards={self.rewards})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def set_claimed(self, index: int, claimed: bool):\n        self.rewards[index].claimed = claimed\n\n    def edit(self, save_file: core.SaveFile):\n        claim_choice = dialog_creator.ChoiceInput.from_reduced(\n            [\"claim\", \"unclaim\", \"fix_claimed\"],\n            dialog=\"claim_or_unclaim_ur\",\n            single_choice=True,\n        ).single_choice()\n\n        if claim_choice is None:\n            return\n\n        claim_choice -= 1\n\n        rank_gifts = core.core_data.get_rank_gifts(save_file)\n        if rank_gifts.rank_gift is None:\n            return\n\n        user_rank = save_file.calculate_user_rank()\n\n        if claim_choice == 2:\n            for rank_gift in rank_gifts.rank_gift:\n                reward = self.rewards[rank_gift.index]\n                if rank_gift.threshold > user_rank:\n                    reward.claimed = False\n\n            color.ColoredText.localize(\"ur_fix_claimed_success\")\n            return\n\n        selected_rank_gifts: list[RankGift] = rank_gifts.rank_gift.copy()\n        descriptions = core.core_data.get_rank_gift_descriptions(save_file)\n\n        selected_rank_gifts.sort(key=lambda rank_gift: rank_gift.threshold)\n\n        new_selected_rank_gifts: list[RankGift] = []\n\n        for rank_gift in selected_rank_gifts:\n            reward = self.rewards[rank_gift.index]\n            if reward.claimed and claim_choice == 0:\n                continue\n            if not reward.claimed and claim_choice == 1:\n                continue\n            if rank_gift.threshold > user_rank:\n                continue\n            new_selected_rank_gifts.append(rank_gift)\n\n        selected_rank_gifts = new_selected_rank_gifts\n\n        selected_descriptions: list[str] = []\n        for rank_gift in selected_rank_gifts:\n            name = rank_gift.get_name(descriptions)\n            if name is None:\n                return\n            description = name.replace(\"<br>\", \" \")\n            # remove span tags\n            start = description.find(\"<\")\n            while start != -1:\n                end = description.find(\">\")\n                description = description[:start] + description[end + 1 :]\n                start = description.find(\"<\")\n\n            selected_descriptions.append(\n                core.core_data.local_manager.get_key(\n                    \"ur_string\",\n                    description=description,\n                    rank=rank_gift.threshold,\n                )\n            )\n\n        ids, _ = dialog_creator.ChoiceInput.from_reduced(\n            selected_descriptions, dialog=\"select_ur\"\n        ).multiple_choice(localized_options=False)\n        if ids is None:\n            return\n        for id in ids:\n            index = selected_rank_gifts[id].index\n            self.set_claimed(index, claim_choice == 0)\n\n        if claim_choice == 0:\n            color.ColoredText.localize(\"ur_claimed_success\")\n        else:\n            color.ColoredText.localize(\"ur_unclaimed_success\")\n\n\ndef edit_user_rank_rewards(save_file: core.SaveFile):\n    user_rank_rewards = save_file.user_rank_rewards\n    user_rank_rewards.edit(save_file)\n"
  },
  {
    "path": "src/bcsfe/core/game/gamoto/__init__.py",
    "content": "from bcsfe.core.game.gamoto import (\n    catamins,\n    gamatoto,\n    base_materials,\n    ototo,\n    cat_shrine,\n)\n\n__all__ = [\"catamins\", \"gamatoto\", \"base_materials\", \"ototo\", \"cat_shrine\"]\n"
  },
  {
    "path": "src/bcsfe/core/game/gamoto/base_materials.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator\n\n\nclass Material:\n    def __init__(self, amount: int):\n        self.amount = amount\n\n    @staticmethod\n    def init() -> Material:\n        return Material(0)\n\n    @staticmethod\n    def read(stream: core.Data) -> Material:\n        amount = stream.read_int()\n        return Material(amount)\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.amount)\n\n    def serialize(self) -> int:\n        return self.amount\n\n    @staticmethod\n    def deserialize(data: int) -> Material:\n        return Material(data)\n\n    def __repr__(self) -> str:\n        return f\"Material(amount={self.amount!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass BaseMaterials:\n    def __init__(self, materials: list[Material]):\n        self.materials = materials\n\n    @staticmethod\n    def init() -> BaseMaterials:\n        return BaseMaterials([])\n\n    @staticmethod\n    def read(stream: core.Data) -> BaseMaterials:\n        total = stream.read_int()\n        materials: list[Material] = []\n        for _ in range(total):\n            materials.append(Material.read(stream))\n        return BaseMaterials(materials)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.materials))\n        for material in self.materials:\n            material.write(stream)\n\n    def serialize(self) -> list[int]:\n        return [material.serialize() for material in self.materials]\n\n    @staticmethod\n    def deserialize(data: list[int]) -> BaseMaterials:\n        return BaseMaterials(\n            [Material.deserialize(material) for material in data]\n        )\n\n    def __repr__(self) -> str:\n        return f\"Materials(materials={self.materials!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def edit_base_materials(self, save_file: core.SaveFile):\n        names = core.core_data.get_gatya_item_names(save_file).names\n        items = core.core_data.get_gatya_item_buy(save_file).get_by_category(7)\n        if items is None:\n            return\n        if names is None:\n            return\n        names = [names[item.id] for item in items]\n        base_materials = [\n            base_material.amount for base_material in self.materials\n        ]\n        values = dialog_creator.MultiEditor.from_reduced(\n            \"base_materials\",\n            names,\n            base_materials,\n            core.core_data.max_value_manager.get(\"base_materials\"),\n            group_name_localized=True,\n        ).edit()\n        self.materials = [Material(value) for value in values]\n"
  },
  {
    "path": "src/bcsfe/core/game/gamoto/cat_shrine.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass CatShrine:\n    def __init__(\n        self,\n        unknown: bool,\n        stamp_1: float,\n        stamp_2: float,\n        shrine_gone: bool,\n        flags: list[int],\n        xp_offering: int,\n    ):\n        self.unknown = unknown\n        self.stamp_1 = stamp_1\n        self.stamp_2 = stamp_2\n        self.shrine_gone = shrine_gone\n        self.flags = flags\n        self.xp_offering = xp_offering\n        self.dialogs = 0\n\n    @staticmethod\n    def init() -> CatShrine:\n        return CatShrine(False, 0.0, 0.0, False, [], 0)\n\n    @staticmethod\n    def read(stream: core.Data) -> CatShrine:\n        unknown = stream.read_bool()\n        stamp_1 = stream.read_double()\n        stamp_2 = stream.read_double()\n        shrine_gone = stream.read_bool()\n        flags = stream.read_byte_list(length=stream.read_byte())\n        xp_offering = stream.read_long()\n        return CatShrine(unknown, stamp_1, stamp_2, shrine_gone, flags, xp_offering)\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.unknown)\n        stream.write_double(self.stamp_1)\n        stream.write_double(self.stamp_2)\n        stream.write_bool(self.shrine_gone)\n        stream.write_byte(len(self.flags))\n        stream.write_byte_list(self.flags, write_length=False)\n        stream.write_long(self.xp_offering)\n\n    def read_dialogs(self, stream: core.Data):\n        self.dialogs = stream.read_int()\n\n    def write_dialogs(self, stream: core.Data):\n        stream.write_int(self.dialogs)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"unknown\": self.unknown,\n            \"stamp_1\": self.stamp_1,\n            \"stamp_2\": self.stamp_2,\n            \"shrine_gone\": self.shrine_gone,\n            \"flags\": self.flags,\n            \"xp_offering\": self.xp_offering,\n            \"dialogs\": self.dialogs,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> CatShrine:\n        shrine = CatShrine(\n            data.get(\"unknown\", False),\n            data.get(\"stamp_1\", 0.0),\n            data.get(\"stamp_2\", 0.0),\n            data.get(\"shrine_gone\", False),\n            data.get(\"flags\", []),\n            data.get(\"xp_offering\", 0),\n        )\n        shrine.dialogs = data.get(\"dialogs\", 0)\n        return shrine\n\n    def __repr__(self):\n        return (\n            f\"CatShrine(\"\n            f\"unknown={self.unknown}, \"\n            f\"stamp_1={self.stamp_1}, \"\n            f\"stamp_2={self.stamp_2}, \"\n            f\"shrine_gone={self.shrine_gone}, \"\n            f\"flags={self.flags}, \"\n            f\"xp_offering={self.xp_offering}, \"\n            f\"dialogs={self.dialogs}\"\n            f\")\"\n        )\n\n    def __str__(self):\n        return self.__repr__()\n\n    @staticmethod\n    def edit_catshrine(save_file: core.SaveFile):\n        shrine = save_file.cat_shrine\n        options = [\n            \"shrine_level\",\n            \"shrine_xp\",\n            \"make_catshrine_appear\",\n            \"make_catshrine_disappear\",\n        ]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"cat_shrine_choice_dialog\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n\n        if choice == 2:\n            shrine.shrine_gone = False\n\n            shrine.stamp_1 = 0.0\n            shrine.stamp_2 = 0.0\n            color.ColoredText.localize(\"cat_shrine_edited\")\n            return\n        elif choice == 3:\n            shrine.shrine_gone = True\n            color.ColoredText.localize(\"cat_shrine_edited\")\n            return\n\n        data = core.core_data.get_cat_shrine_levels(save_file)\n\n        xp = shrine.xp_offering\n        level = data.get_level_from_xp(xp)\n\n        color.ColoredText.localize(\"current_shrine_xp_level\", level=level, xp=xp)\n\n        if choice == 0:\n            max_level = data.get_max_level()\n            if max_level is None:\n                return\n            level = dialog_creator.IntInput(\n                min=1, max=max_level\n            ).get_input_locale_while(\"shrine_level_dialog\", {\"max_level\": max_level})\n            if level is None:\n                return\n            shrine.xp_offering = data.get_xp_from_level(level)\n        elif choice == 1:\n            max_xp = data.get_max_xp()\n            if max_xp is None:\n                return\n            xp = dialog_creator.IntInput(min=0, max=max_xp).get_input_locale_while(\n                \"shrine_xp_dialog\", {\"max_xp\": max_xp}\n            )\n            if xp is None:\n                return\n            shrine.xp_offering = xp\n\n        xp = shrine.xp_offering\n        if xp is None:\n            return\n        level = data.get_level_from_xp(xp)\n        if level is None:\n            return\n\n        shrine.dialogs = level - 1\n        shrine.shrine_gone = False\n        shrine.stamp_1 = 0.0\n        shrine.stamp_2 = 0.0\n\n        color.ColoredText.localize(\"current_shrine_xp_level\", level=level, xp=xp)\n\n        color.ColoredText.localize(\"cat_shrine_edited\")\n\n\nclass CatShrineLevels:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.boundaries = self.get_boundaries()\n\n    def get_boundaries(self) -> list[int] | None:\n        file_name = \"jinja_level.csv\"\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"resLocal\", file_name)\n        if data is None:\n            return None\n        csv = core.CSV(\n            data,\n            delimiter=core.Delimeter.from_country_code_res(self.save_file.cc),\n        )\n        boundaries: list[int] = []\n        counter = 0\n        for row in csv:\n            xp = row[0].to_int()\n            counter += xp\n            boundaries.append(counter)\n\n        return boundaries\n\n    def get_level_from_xp(self, xp: int) -> int | None:\n        if self.boundaries is None:\n            return None\n        for i, boundary in enumerate(self.boundaries):\n            if xp < boundary:\n                return i + 1\n        return len(self.boundaries)\n\n    def get_xp_from_level(self, level: int) -> int | None:\n        if self.boundaries is None:\n            return None\n        if level < 1:\n            return 0\n        if level > len(self.boundaries):\n            return self.get_max_xp()\n        return self.boundaries[level - 2]\n\n    def get_max_level(self) -> int | None:\n        if self.boundaries is None:\n            return None\n        return len(self.boundaries)\n\n    def get_max_xp(self) -> int | None:\n        if self.boundaries is None:\n            return None\n        return max(self.boundaries)\n"
  },
  {
    "path": "src/bcsfe/core/game/gamoto/catamins.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\n\nclass Catamin:\n    def __init__(self, amount: int):\n        self.amount = amount\n\n    @staticmethod\n    def read(stream: core.Data) -> Catamin:\n        amount = stream.read_int()\n        return Catamin(amount)\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.amount)\n\n    def serialize(self) -> int:\n        return self.amount\n\n    @staticmethod\n    def deserialize(data: int) -> Catamin:\n        return Catamin(data)\n\n    def __repr__(self):\n        return f\"Catamin({self.amount})\"\n\n    def __str__(self):\n        return f\"Catamin({self.amount})\"\n\n\nclass Catamins:\n    def __init__(self, catamins: list[Catamin]):\n        self.catamins = catamins\n\n    @staticmethod\n    def read(stream: core.Data) -> Catamins:\n        total = stream.read_int()\n        catamins: list[Catamin] = []\n        for _ in range(total):\n            catamins.append(Catamin.read(stream))\n        return Catamins(catamins)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.catamins))\n        for catamin in self.catamins:\n            catamin.write(stream)\n\n    def serialize(self) -> list[int]:\n        return [catamin.serialize() for catamin in self.catamins]\n\n    @staticmethod\n    def deserialize(data: list[int]) -> Catamins:\n        return Catamins([Catamin.deserialize(catamin) for catamin in data])\n\n    def __repr__(self):\n        return f\"Catamins({self.catamins})\"\n\n    def __str__(self):\n        return f\"Catamins({self.catamins})\"\n"
  },
  {
    "path": "src/bcsfe/core/game/gamoto/gamatoto.py",
    "content": "from __future__ import annotations\nfrom dataclasses import dataclass\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\n@dataclass\nclass MemberName:\n    member_id: int\n    rarity: int\n    bonus: int\n    name: str\n    rarity_name: str\n    description: list[str]\n\n\nclass GamatotoMembersName:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.members = self.read_members()\n\n    def read_members(self) -> list[MemberName] | None:\n        members: list[MemberName] = []\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\n            \"resLocal\",\n            f\"GamatotoExpedition_Members_name_{core.core_data.get_lang(self.save_file)}.csv\",\n        )\n        if data is None:\n            return None\n        csv = core.CSV(\n            data,\n            delimiter=core.Delimeter.from_country_code_res(self.save_file.cc),\n            remove_empty=False,\n        )\n        for line in csv.lines[1:]:\n            if line[0].to_int() == -1:\n                continue\n            members.append(\n                MemberName(\n                    line[0].to_int(),\n                    line[1].to_int(),\n                    line[2].to_int(),\n                    line[3].to_str(),\n                    line[4].to_str(),\n                    line[5:].to_str_list(),\n                )\n            )\n        return members\n\n    def get_member(self, member_id: int) -> MemberName | None:\n        if self.members is None:\n            return None\n        for member in self.members:\n            if member.member_id == member_id:\n                return member\n        return None\n\n    def get_members_from_ids(self, ids: list[int]) -> list[MemberName | None]:\n        return [self.get_member(id) for id in ids]\n\n    def get_all_rarity(self, rarity: int) -> list[MemberName] | None:\n        if self.members is None:\n            return None\n\n        return [member for member in self.members if member.rarity == rarity]\n\n    def get_members_from_helpers(\n        self, helpers: Helpers\n    ) -> list[MemberName | None]:\n        return self.get_members_from_ids(\n            [helper.id for helper in helpers.helpers if helper.is_valid()]\n        )\n\n    def get_all_rarity_names(self) -> list[str] | None:\n        if self.members is None:\n            return None\n        names: dict[int, str] = {}\n        for member in self.members:\n            names[member.rarity] = member.rarity_name\n        return [names[i] for i in range(len(names))]\n\n\n@dataclass\nclass GamatotoLevel:\n    level: int\n    xp_needed: int\n    discovery_bonus: int\n    skin: int\n\n\n@dataclass\nclass GamatotoLimit:\n    max_level: int\n    total_stages: int\n    total_helpers: int\n\n\nclass GamatotoLevels:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.levels = self.read_levels()\n        self.limit = self.read_max_level()\n\n    def read_levels(self) -> list[GamatotoLevel] | None:\n        levels: list[GamatotoLevel] = []\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"GamatotoExpedition.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        for i, line in enumerate(csv):\n            levels.append(\n                GamatotoLevel(\n                    i + 1, line[0].to_int(), line[1].to_int(), line[2].to_int()\n                )\n            )\n        return levels\n\n    def read_max_level(self) -> GamatotoLimit | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"GamatotoExpedition_Limit.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        line = csv[0]\n        return GamatotoLimit(\n            line[0].to_int(), line[1].to_int(), line[2].to_int()\n        )\n\n    def get_level(self, level: int) -> GamatotoLevel | None:\n        if self.levels is None:\n            return None\n        if level < 1:\n            return None\n        return self.levels[level - 1]\n\n    def get_all_levels(self) -> list[GamatotoLevel] | None:\n        return self.levels\n\n    def get_level_from_xp(self, xp: int) -> GamatotoLevel | None:\n        if self.levels is None or self.limit is None:\n            return None\n        for level in self.levels:\n            if level.level >= self.limit.max_level:\n                break\n            if level.xp_needed == -1:\n                continue\n            if xp < level.xp_needed:\n                return level\n        if self.limit.max_level >= len(self.levels):\n            return self.levels[-1]\n        return self.levels[self.limit.max_level - 1]\n\n    def get_xp_from_level(self, level: int) -> int | None:\n        if self.levels is None:\n            return None\n        level -= 1\n        if level < 1:\n            return 0\n        return self.levels[level - 1].xp_needed\n\n    def get_max_level(self) -> int | None:\n        if self.limit is None:\n            return None\n        return self.limit.max_level\n\n    def get_total_stages(self) -> int | None:\n        if self.limit is None:\n            return None\n        return self.limit.total_stages\n\n    def get_total_helpers(self) -> int | None:\n        if self.limit is None:\n            return None\n        return self.limit.total_helpers\n\n\nclass Helper:\n    def __init__(self, id: int):\n        self.id = id\n\n    @staticmethod\n    def init() -> Helper:\n        return Helper(-1)\n\n    @staticmethod\n    def read(stream: core.Data) -> Helper:\n        id = stream.read_int()\n        return Helper(id)\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.id)\n\n    def serialize(self) -> int:\n        return self.id\n\n    @staticmethod\n    def deserialize(data: int) -> Helper:\n        return Helper(data)\n\n    def __repr__(self) -> str:\n        return f\"Helper(id={self.id!r})\"\n\n    def __str__(self) -> str:\n        return f\"Helper(id={self.id!r})\"\n\n    def is_valid(self) -> bool:\n        return self.id != -1\n\n\nclass Helpers:\n    def __init__(self, helpers: list[Helper]):\n        self.helpers = helpers\n\n    @staticmethod\n    def init() -> Helpers:\n        return Helpers([])\n\n    @staticmethod\n    def read(stream: core.Data) -> Helpers:\n        total = stream.read_int()\n        helpers: list[Helper] = []\n        for _ in range(total):\n            helpers.append(Helper.read(stream))\n        return Helpers(helpers)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.helpers))\n        for helper in self.helpers:\n            helper.write(stream)\n\n    def serialize(self) -> list[int]:\n        return [helper.serialize() for helper in self.helpers]\n\n    @staticmethod\n    def deserialize(data: list[int]) -> Helpers:\n        return Helpers([Helper.deserialize(helper) for helper in data])\n\n    def __repr__(self) -> str:\n        return f\"Helpers(helpers={self.helpers!r})\"\n\n    def __str__(self) -> str:\n        return f\"Helpers(helpers={self.helpers!r})\"\n\n\nclass Gamatoto:\n    def __init__(\n        self,\n        remaining_seconds: float,\n        return_flag: bool,\n        xp: int,\n        dest_id: int,\n        recon_length: int,\n        unknown: int,\n        notif_value: int,\n    ):\n        self.remaining_seconds = remaining_seconds\n        self.return_flag = return_flag\n        self.xp = xp\n        self.dest_id = dest_id\n        self.recon_length = recon_length\n        self.unknown = unknown\n        self.notif_value = notif_value\n        self.helpers = Helpers.init()\n        self.is_ad_present = False\n        self.skin = 0\n        self.collab_flags: dict[int, bool] = {}\n        self.collab_durations: dict[int, float] = {}\n\n    @staticmethod\n    def init() -> Gamatoto:\n        return Gamatoto(\n            0.0,\n            False,\n            0,\n            0,\n            0,\n            0,\n            0,\n        )\n\n    @staticmethod\n    def read(stream: core.Data) -> Gamatoto:\n        remaining_seconds = stream.read_double()\n        return_flag = stream.read_bool()\n        xp = stream.read_int()\n        dest_id = stream.read_int()\n        recon_length = stream.read_int()\n        unknown = stream.read_int()\n        notif_value = stream.read_int()\n        return Gamatoto(\n            remaining_seconds,\n            return_flag,\n            xp,\n            dest_id,\n            recon_length,\n            unknown,\n            notif_value,\n        )\n\n    def write(self, stream: core.Data):\n        stream.write_double(self.remaining_seconds)\n        stream.write_bool(self.return_flag)\n        stream.write_int(self.xp)\n        stream.write_int(self.dest_id)\n        stream.write_int(self.recon_length)\n        stream.write_int(self.unknown)\n        stream.write_int(self.notif_value)\n\n    def read_2(self, stream: core.Data):\n        self.helpers = Helpers.read(stream)\n        self.is_ad_present = stream.read_bool()\n\n    def write_2(self, stream: core.Data):\n        self.helpers.write(stream)\n        stream.write_bool(self.is_ad_present)\n\n    def read_skin(self, stream: core.Data):\n        self.skin = stream.read_int()\n\n    def write_skin(self, stream: core.Data):\n        stream.write_int(self.skin)\n\n    def read_collab_data(self, stream: core.Data):\n        self.collab_flags: dict[int, bool] = stream.read_int_bool_dict()\n        self.collab_durations: dict[int, float] = stream.read_int_double_dict()\n\n    def write_collab_data(self, stream: core.Data):\n        stream.write_int_bool_dict(self.collab_flags)\n        stream.write_int_double_dict(self.collab_durations)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"remaining_seconds\": self.remaining_seconds,\n            \"return_flag\": self.return_flag,\n            \"xp\": self.xp,\n            \"dest_id\": self.dest_id,\n            \"recon_length\": self.recon_length,\n            \"unknown\": self.unknown,\n            \"notif_value\": self.notif_value,\n            \"helpers\": self.helpers.serialize(),\n            \"is_ad_present\": self.is_ad_present,\n            \"skin\": self.skin,\n            \"collab_flags\": self.collab_flags,\n            \"collab_durations\": self.collab_durations,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Gamatoto:\n        gamatoto = Gamatoto(\n            data.get(\"remaining_seconds\", 0.0),\n            data.get(\"return_flag\", False),\n            data.get(\"xp\", 0),\n            data.get(\"dest_id\", 0),\n            data.get(\"recon_length\", 0),\n            data.get(\"unknown\", 0),\n            data.get(\"notif_value\", 0),\n        )\n        gamatoto.helpers = Helpers.deserialize(data.get(\"helpers\", []))\n        gamatoto.is_ad_present = data.get(\"is_ad_present\", False)\n        gamatoto.skin = data.get(\"skin\", 0)\n        gamatoto.collab_flags = data.get(\"collab_flags\", {})\n        gamatoto.collab_durations = data.get(\"collab_durations\", {})\n        return gamatoto\n\n    def __repr__(self):\n        return (\n            f\"Gamatoto(remaining_seconds={self.remaining_seconds!r}, \"\n            f\"return_flag={self.return_flag!r}, xp={self.xp!r}, \"\n            f\"dest_id={self.dest_id!r}, recon_length={self.recon_length!r}, \"\n            f\"unknown={self.unknown!r}, notif_value={self.notif_value!r}, \"\n            f\"helpers={self.helpers!r}, is_ad_present={self.is_ad_present!r}, \"\n            f\"skin={self.skin!r}, collab_flags={self.collab_flags!r}, \"\n            f\"collab_durations={self.collab_durations!r})\"\n        )\n\n    def __str__(self):\n        return self.__repr__()\n\n    def edit_xp(self, save_file: core.SaveFile):\n        gamatoto_levels = core.core_data.get_gamatoto_levels(save_file)\n        current_level = gamatoto_levels.get_level_from_xp(self.xp)\n        if current_level is None:\n            return\n        xp = self.xp\n\n        color.ColoredText.localize(\n            \"gamatoto_level_current\", level=current_level.level, xp=xp\n        )\n        choice = dialog_creator.ChoiceInput(\n            [\"enter_raw_gamatoto_xp\", \"enter_gamatoto_level\"],\n            [\"enter_raw_gamatoto_xp\", \"enter_gamatoto_level\"],\n            [],\n            {},\n            \"edit_gamatoto_level_q\",\n            single_choice=True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n\n        if choice == 0:\n            xp = dialog_creator.SingleEditor(\n                \"gamatoto_xp\", self.xp, None, localized_item=True\n            ).edit()\n            current_level = gamatoto_levels.get_level_from_xp(xp)\n        elif choice == 1:\n            value = dialog_creator.SingleEditor(\n                \"gamatoto_level\",\n                current_level.level,\n                gamatoto_levels.get_max_level(),\n                localized_item=True,\n            ).edit()\n            xp = gamatoto_levels.get_xp_from_level(value)\n            current_level = gamatoto_levels.get_level(value)\n\n        if xp is None:\n            return\n\n        self.xp = xp\n\n        if current_level is None:\n            return\n\n        color.ColoredText.localize(\n            \"gamatoto_level_success\", level=current_level.level, xp=xp\n        )\n\n    def edit_helpers(self, save_file: core.SaveFile):\n        members_name = core.core_data.get_gamatoto_members_name(save_file)\n\n        gamatoto_levels = core.core_data.get_gamatoto_levels(save_file)\n        max_helpers = gamatoto_levels.get_total_helpers()\n\n        members = members_name.get_members_from_helpers(self.helpers)\n        color.ColoredText.localize(\"current_gamatoto_helpers\")\n        for member in members:\n            if member is None:\n                continue\n            color.ColoredText.localize(\n                \"gamatoto_helper\",\n                name=member.name,\n                rarity_name=member.rarity_name,\n            )\n        rarity_names = members_name.get_all_rarity_names()\n        if rarity_names is None:\n            return\n\n        total_rarity_amounts: list[int] = [0] * len(rarity_names)\n        for helper in self.helpers.helpers:\n            if not helper.is_valid():\n                continue\n            member = members_name.get_member(helper.id)\n            if member is None:\n                continue\n            total_rarity_amounts[member.rarity] += 1\n\n        rarity_amounts = dialog_creator.MultiEditor.from_reduced(\n            \"gamatoto_helpers\",\n            rarity_names,\n            total_rarity_amounts,\n            max_helpers,\n            group_name_localized=True,\n            cumulative_max=True,\n        ).edit()\n\n        helpers: list[Helper] = []\n        for i, rarity_amount in enumerate(rarity_amounts):\n            rarity_members = members_name.get_all_rarity(i)\n            if rarity_members is None:\n                continue\n            for _ in range(rarity_amount):\n                member = rarity_members.pop(0)\n                helpers.append(Helper(member.member_id))\n        self.helpers = Helpers(helpers)\n\n        members = members_name.get_members_from_helpers(self.helpers)\n        color.ColoredText.localize(\"new_gamatoto_helpers\")\n        for member in members:\n            if member is None:\n                continue\n            color.ColoredText.localize(\n                \"gamatoto_helper\",\n                name=member.name,\n                rarity_name=member.rarity_name,\n            )\n\n\ndef edit_xp(save_file: core.SaveFile):\n    save_file.gamatoto.edit_xp(save_file)\n\n\ndef edit_helpers(save_file: core.SaveFile):\n    save_file.gamatoto.edit_helpers(save_file)\n"
  },
  {
    "path": "src/bcsfe/core/game/gamoto/ototo.py",
    "content": "from __future__ import annotations\nfrom dataclasses import dataclass\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color\n\n\n@dataclass\nclass LevelPartRecipeUnlock:\n    index: int\n    cannon_id: int\n    part_id: int\n    unknown: int\n    unknown2: int\n    level: int\n\n\nclass CastleRecipeUnlock:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.level_part_recipe_unlocks = self.get_recipe_unlocks()\n\n    def get_recipe_unlocks(self) -> list[LevelPartRecipeUnlock] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"DataLocal\", \"CastleRecipeUnlock.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(data)\n        level_part_recipe_unlocks: list[LevelPartRecipeUnlock] = []\n        for i, line in enumerate(csv):\n            level_part_recipe_unlocks.append(\n                LevelPartRecipeUnlock(\n                    index=i,\n                    cannon_id=line[0].to_int(),\n                    part_id=line[1].to_int(),\n                    unknown=line[2].to_int(),\n                    unknown2=line[3].to_int(),\n                    level=line[4].to_int(),\n                )\n            )\n\n        return level_part_recipe_unlocks\n\n    def get_recipe_unlock(self, index: int) -> LevelPartRecipeUnlock | None:\n        if self.level_part_recipe_unlocks is None:\n            return None\n        for recipe_unlock in self.level_part_recipe_unlocks:\n            if recipe_unlock.index == index:\n                return recipe_unlock\n\n        return None\n\n    def get_max_level(self, cannon_id: int, part_id: int) -> int | None:\n        if self.level_part_recipe_unlocks is None:\n            return None\n        max_level = 0\n\n        for recipe_unlock in self.level_part_recipe_unlocks:\n            if (\n                recipe_unlock.cannon_id == cannon_id\n                and recipe_unlock.part_id == part_id\n            ):\n                if recipe_unlock.level > max_level:\n                    max_level = recipe_unlock.level\n\n        return max_level\n\n    def get_max_part_level(self, part_id: int) -> int | None:\n        if self.level_part_recipe_unlocks is None:\n            return None\n        max_level = 0\n        for recipe_unlock in self.level_part_recipe_unlocks:\n            if recipe_unlock.part_id == part_id:\n                if recipe_unlock.level > max_level:\n                    max_level = recipe_unlock.level\n\n        return max_level\n\n\n@dataclass\nclass CannonDescription:\n    cannon_id: int\n    build_name: str\n    foundation_build_description: str\n    style_build_description: str\n    effect_build_description: str\n    cannon_build_description: str\n    cannon_name: str\n    foundation_name: str\n    style_name: str\n    effect_description: str\n    improve_foundation_description: str\n    improve_style_description: str\n    improved_foundation_name: str\n    improved_style_name: str\n    improved_effect1_description: str\n    improved_effect2_description: str\n\n    def get_part_names(self) -> list[str]:\n        effect_name = self.effect_build_description.split(\"<br>\")[0]\n        if not effect_name:\n            effect_name = self.build_name\n        return [\n            effect_name,\n            self.improve_foundation_description.split(\"<br>\")[0],\n            self.improve_style_description.split(\"<br>\")[0],\n        ]\n\n    def get_part_name(self, index: int) -> str:\n        return self.get_part_names()[index]\n\n    def get_longest_part_name(self) -> str:\n        return max(self.get_part_names(), key=len)\n\n    def get_cannon_name(self) -> str:\n        return self.cannon_name\n\n\nclass CannonDescriptions:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.cannon_descriptions = self.get_cannon_descriptions()\n\n    def get_cannon_descriptions(self) -> list[CannonDescription] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"resLocal\", \"CastleRecipeDescriptions.csv\")\n        if data is None:\n            return None\n        csv = core.CSV(\n            data,\n            delimiter=core.Delimeter.from_country_code_res(self.save_file.cc),\n            remove_empty=False,\n        )\n        cannon_descriptions: list[CannonDescription] = []\n        for line in csv:\n            cannon_descriptions.append(\n                CannonDescription(\n                    cannon_id=line[0].to_int(),\n                    build_name=line[1].to_str(),\n                    foundation_build_description=line[2].to_str(),\n                    style_build_description=line[3].to_str(),\n                    effect_build_description=line[4].to_str(),\n                    cannon_build_description=line[5].to_str(),\n                    cannon_name=line[6].to_str(),\n                    foundation_name=line[7].to_str(),\n                    style_name=line[8].to_str(),\n                    effect_description=line[9].to_str(),\n                    improve_foundation_description=line[10].to_str(),\n                    improve_style_description=line[11].to_str(),\n                    improved_foundation_name=line[12].to_str(),\n                    improved_style_name=line[13].to_str(),\n                    improved_effect1_description=line[14].to_str(),\n                    improved_effect2_description=line[15].to_str(),\n                )\n            )\n\n        return cannon_descriptions\n\n    def get_cannon_description(\n        self, cannon_id: int\n    ) -> CannonDescription | None:\n        if self.cannon_descriptions is None:\n            return None\n        for cannon_description in self.cannon_descriptions:\n            if cannon_description.cannon_id == cannon_id:\n                return cannon_description\n\n        return None\n\n    def get_longest_longest_part_name(self) -> str | None:\n        if self.cannon_descriptions is None:\n            return None\n        longest_part_name = \"\"\n        for cannon_description in self.cannon_descriptions:\n            l_name = cannon_description.get_longest_part_name()\n            if len(l_name) > len(longest_part_name):\n                longest_part_name = l_name\n\n        return longest_part_name\n\n\nclass Cannon:\n    def __init__(self, development: int, levels: list[int]):\n        self.development = development\n        self.levels = levels\n\n    @staticmethod\n    def init() -> Cannon:\n        return Cannon(0, [])\n\n    @staticmethod\n    def read(stream: core.Data) -> Cannon:\n        total = stream.read_int()\n        levels: list[int] = []\n        development = stream.read_int()\n        for _ in range(total - 1):\n            levels.append(stream.read_int())\n        return Cannon(development, levels)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.levels) + 1)\n        stream.write_int(self.development)\n        for level in self.levels:\n            stream.write_int(level)\n\n    def serialize(self) -> list[int]:\n        return [self.development] + self.levels\n\n    @staticmethod\n    def deserialize(data: list[int]) -> Cannon:\n        return Cannon(data[0], data[1:])\n\n    def __repr__(self):\n        return f\"Cannon({self.development}, {self.levels})\"\n\n    def __str__(self):\n        return f\"Cannon({self.development}, {self.levels})\"\n\n\nclass Cannons:\n    def __init__(\n        self, cannons: dict[int, Cannon], selected_parts: list[list[int]]\n    ):\n        self.cannons = cannons\n        self.selected_parts = selected_parts\n\n    @staticmethod\n    def init(gv: core.GameVersion) -> Cannons:\n        cannnons = {}\n        if gv < 80200:\n            selected_parts = [[0, 0, 0]]\n        else:\n            if gv > 90699:\n                total_selected_parts = 0\n            else:\n                total_selected_parts = 10\n\n            selected_parts = [[0, 0, 0] for _ in range(total_selected_parts)]\n        return Cannons(cannnons, selected_parts)\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> Cannons:\n        total = stream.read_int()\n        cannons: dict[int, Cannon] = {}\n        for _ in range(total):\n            cannon_id = stream.read_int()\n            cannon = Cannon.read(stream)\n            cannons[cannon_id] = cannon\n        if gv < 80200:\n            selected_parts = [stream.read_int_list(length=3)]\n        else:\n            if gv > 90699:\n                total_selected_parts = stream.read_byte()\n            else:\n                total_selected_parts = 10\n\n            selected_parts: list[list[int]] = []\n            for _ in range(total_selected_parts):\n                selected_parts.append(stream.read_byte_list(length=3))\n\n        return Cannons(cannons, selected_parts)\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        stream.write_int(len(self.cannons))\n        for cannon_id, cannon in self.cannons.items():\n            stream.write_int(cannon_id)\n            cannon.write(stream)\n        if gv < 80200:\n            stream.write_int_list(\n                self.selected_parts[0], write_length=False, length=3\n            )\n        else:\n            if gv > 90699:\n                stream.write_byte(len(self.selected_parts))\n\n            for part in self.selected_parts:\n                stream.write_byte_list(part, write_length=False, length=3)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"cannons\": {\n                cannon_id: cannon.serialize()\n                for cannon_id, cannon in self.cannons.items()\n            },\n            \"selected_parts\": self.selected_parts,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Cannons:\n        return Cannons(\n            {\n                cannon_id: Cannon.deserialize(cannon)\n                for cannon_id, cannon in data.get(\"cannons\", {}).items()\n            },\n            data.get(\"selected_parts\", []),\n        )\n\n    def __repr__(self):\n        return f\"Cannons({self.cannons}, {self.selected_parts})\"\n\n    def __str__(self):\n        return f\"Cannons({self.cannons}, {self.selected_parts})\"\n\n\nclass Ototo:\n    def __init__(\n        self,\n        base_materials: core.BaseMaterials,\n        game_version: core.GameVersion | None = None,\n    ):\n        self.base_materials = base_materials\n        self.remaining_seconds = 0.0\n        self.return_flag = False\n        self.improve_id = 0\n        self.engineers = 0\n        self.cannons = Cannons.init(game_version) if game_version else None\n\n    @staticmethod\n    def init(game_version: core.GameVersion) -> Ototo:\n        return Ototo(core.BaseMaterials.init(), game_version)\n\n    @staticmethod\n    def read(stream: core.Data) -> Ototo:\n        bm = core.BaseMaterials.read(stream)\n        return Ototo(bm)\n\n    def write(self, stream: core.Data):\n        self.base_materials.write(stream)\n\n    def read_2(self, stream: core.Data, gv: core.GameVersion):\n        self.remaining_seconds = stream.read_double()\n        self.return_flag = stream.read_bool()\n        self.improve_id = stream.read_int()\n        self.engineers = stream.read_int()\n        self.cannons = Cannons.read(stream, gv)\n\n    def write_2(self, stream: core.Data, gv: core.GameVersion):\n        stream.write_double(self.remaining_seconds)\n        stream.write_bool(self.return_flag)\n        stream.write_int(self.improve_id)\n        stream.write_int(self.engineers)\n        if self.cannons is None:\n            Cannons.init(gv).write(stream, gv)\n        else:\n            self.cannons.write(stream, gv)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"base_materials\": self.base_materials.serialize(),\n            \"remaining_seconds\": self.remaining_seconds,\n            \"return_flag\": self.return_flag,\n            \"improve_id\": self.improve_id,\n            \"engineers\": self.engineers,\n            \"cannons\": self.cannons.serialize() if self.cannons else None,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Ototo:\n        ototo = Ototo(\n            core.BaseMaterials.deserialize(data.get(\"base_materials\", []))\n        )\n        ototo.remaining_seconds = data.get(\"remaining_seconds\", 0.0)\n        ototo.return_flag = data.get(\"return_flag\", False)\n        ototo.improve_id = data.get(\"improve_id\", 0)\n        ototo.engineers = data.get(\"engineers\", 0)\n        ototo.cannons = Cannons.deserialize(data.get(\"cannons\", {}))\n        return ototo\n\n    def __repr__(self):\n        return f\"Ototo({self.base_materials}, {self.remaining_seconds}, {self.return_flag}, {self.improve_id}, {self.engineers}, {self.cannons})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    @staticmethod\n    def get_max_engineers(save_file: core.SaveFile) -> int:\n        file = core.core_data.get_game_data_getter(save_file).download(\n            \"DataLocal\", \"CastleCustomLimit.csv\"\n        )\n        if file is None:\n            return 5\n        csv = core.CSV(file)\n        return csv.lines[0][0].to_int()\n\n    def edit_engineers(self, save_file: core.SaveFile):\n        name = core.core_data.get_gatya_item_names(save_file).get_name(92)\n        if name is None:\n            name = \"engineers\"\n            localized_item = True\n        else:\n            localized_item = False\n        self.engineers = dialog_creator.SingleEditor(\n            name,\n            self.engineers,\n            Ototo.get_max_engineers(save_file),\n            localized_item=localized_item,\n        ).edit()\n\n    def display_current_cannons(\n        self, save_file: core.SaveFile\n    ) -> list[str] | None:\n        descriptions = CannonDescriptions(save_file)\n        recipe_unlocks = CastleRecipeUnlock(save_file)\n\n        color.ColoredText.localize(\"current_cannon_stats\")\n\n        if self.cannons is None:\n            self.cannons = Cannons.init(save_file.game_version)\n\n        names: list[str] = []\n        longest_part_name = descriptions.get_longest_longest_part_name()\n        if longest_part_name is None:\n            return None\n        longest_part_name = len(longest_part_name)\n\n        for cannon_id, cannon in self.cannons.cannons.items():\n            description = descriptions.get_cannon_description(cannon_id)\n            if description is None:\n                continue\n            recipe_unlock = recipe_unlocks.get_recipe_unlock(cannon_id)\n            if recipe_unlock is None:\n                continue\n            cannon_name = description.get_cannon_name()\n            names.append(cannon_name)\n            text = cannon_name\n            if cannon_id != 0:\n                cannon_name_length = len(cannon_name) - 10\n                buffer = \" \" * (longest_part_name - cannon_name_length)\n                text += core.core_data.local_manager.get_key(\n                    \"development\",\n                    development=Ototo.get_stage_name(cannon.development),\n                    escape=False,\n                    buffer=buffer,\n                )\n\n            for part_id, level in enumerate(cannon.levels):\n                if part_id == 0:\n                    level += 1\n\n                text += \"\\n\"\n                text += \"        \"\n                buffer = \" \" * (\n                    longest_part_name\n                    - len(description.get_part_name(part_id))\n                    + 2\n                )\n                name = description.get_part_name(part_id)\n                text += core.core_data.local_manager.get_key(\n                    \"cannon_part\", name=name, level=level, buffer=buffer\n                )\n\n            text += \"\\n\"\n\n            color.ColoredText.localize(\"cannon_stats\", parts=text, escape=False)\n\n        return names\n\n    def edit_cannon(self, save_file: core.SaveFile):\n        if self.cannons is None:\n            self.cannons = Cannons.init(save_file.game_version)\n\n        names = self.display_current_cannons(save_file)\n        if names is None:\n            return\n\n        cannon_ids, all_at_once = dialog_creator.ChoiceInput.from_reduced(\n            names, dialog=\"select_cannon\"\n        ).multiple_choice()\n        if cannon_ids is None:\n            return\n\n        if len(cannon_ids) > 1 and not all_at_once:\n            choice = dialog_creator.ChoiceInput.from_reduced(\n                [\"individual\", \"edit_all_at_once\"],\n                dialog=\"cannon_edit_type\",\n                single_choice=True,\n            ).single_choice()\n            if choice is None:\n                return\n            choice -= 1\n            if choice == 0:\n                all_at_once = False\n            else:\n                all_at_once = True\n\n        if len(cannon_ids) > 1 or (len(cannon_ids) == 1 and cannon_ids[0] != 0):\n            choice = dialog_creator.ChoiceInput.from_reduced(\n                [\"development_o\", \"level_o\"],\n                dialog=\"cannon_dev_level_q\",\n                single_choice=True,\n            ).single_choice()\n            if choice is None:\n                return\n            choice -= 1\n        else:\n            choice = 1\n        if choice == 0:\n            self.edit_cannon_development(save_file, all_at_once, cannon_ids)\n        elif choice == 1:\n            self.edit_cannon_level(save_file, all_at_once, cannon_ids)\n\n        color.ColoredText.localize(\"cannon_success\")\n\n        self.display_current_cannons(save_file)\n\n    def select_development(self) -> int | None:\n        return dialog_creator.ChoiceInput.from_reduced(\n            [\"none\", \"foundation\", \"style\", \"effect\"],\n            dialog=\"select_development\",\n            single_choice=True,\n        ).single_choice()\n\n    def edit_cannon_development(\n        self, save_file: core.SaveFile, all_at_once: bool, cannon_ids: list[int]\n    ):\n        if self.cannons is None:\n            self.cannons = Cannons.init(save_file.game_version)\n        if all_at_once:\n            development = self.select_development()\n            if development is None:\n                return\n            for cannon_id in cannon_ids:\n                if cannon_id == 0:\n                    continue\n                self.cannons.cannons[cannon_id].development = development - 1\n        else:\n            for cannon_id in cannon_ids:\n                if cannon_id == 0:\n                    continue\n                cannon_description = CannonDescriptions(\n                    save_file\n                ).get_cannon_description(cannon_id)\n                if cannon_description is None:\n                    continue\n                current_development = self.cannons.cannons[\n                    cannon_id\n                ].development\n\n                color.ColoredText.localize(\n                    \"selected_cannon_stage\",\n                    name=cannon_description.get_cannon_name(),\n                    stage=Ototo.get_stage_name(current_development),\n                    escape=False,\n                )\n                development = self.select_development()\n                if development is None:\n                    return\n                self.cannons.cannons[cannon_id].development = development - 1\n\n    def edit_cannon_level(\n        self, save_file: core.SaveFile, all_at_once: bool, cannon_ids: list[int]\n    ):\n        if self.cannons is None:\n            self.cannons = Cannons.init(save_file.game_version)\n        cannon_descriptions = CannonDescriptions(save_file)\n        cannon_recipe = CastleRecipeUnlock(save_file)\n        if all_at_once:\n            max_part_level_0 = cannon_recipe.get_max_part_level(0)\n            max_part_level_1 = cannon_recipe.get_max_part_level(1)\n            max_part_level_2 = cannon_recipe.get_max_part_level(2)\n            if (\n                max_part_level_0 is None\n                or max_part_level_1 is None\n                or max_part_level_2 is None\n            ):\n                return\n            levels = dialog_creator.MultiEditor.from_reduced(\n                \"cannon_level\",\n                [\"effect\", \"improved_foundation\", \"improved_style\"],\n                None,\n                max_values=[\n                    max_part_level_0,\n                    max_part_level_1,\n                    max_part_level_2,\n                ],\n                group_name_localized=True,\n                items_localized=True,\n            ).edit()\n            if not levels:\n                return\n            for cannon_id in cannon_ids:\n                cannon = self.get_cannon(cannon_id)\n                if cannon is None:\n                    continue\n                cannon.development = max(cannon.development, 3)\n\n                for part_id, level in enumerate(levels):\n                    if part_id == 0:\n                        level -= 1\n                    max_level = cannon_recipe.get_max_level(cannon_id, part_id)\n                    if max_level is None:\n                        continue\n                    if part_id >= len(cannon.levels):\n                        break\n                    cannon.levels[part_id] = min(level, max_level)\n        else:\n            for cannon_id in cannon_ids:\n                cannon = self.get_cannon(cannon_id)\n                if cannon is None:\n                    continue\n                cannon.development = max(cannon.development, 3)\n\n                cannon_desc = cannon_descriptions.get_cannon_description(\n                    cannon_id\n                )\n                if cannon_desc is None:\n                    continue\n                levels = cannon.levels\n                levels[0] += 1\n                names = [\"effect\", \"improved_foundation\", \"improved_style\"]\n                if cannon_id == 0:\n                    names = [\"effect\"]\n                max_part_level_0 = cannon_recipe.get_max_part_level(0)\n                max_part_level_1 = cannon_recipe.get_max_part_level(1)\n                max_part_level_2 = cannon_recipe.get_max_part_level(2)\n                if (\n                    max_part_level_0 is None\n                    or max_part_level_1 is None\n                    or max_part_level_2 is None\n                ):\n                    return\n\n                levels = dialog_creator.MultiEditor.from_reduced(\n                    cannon_desc.get_cannon_name(),\n                    names,\n                    levels,\n                    max_values=[\n                        max_part_level_0,\n                        max_part_level_1,\n                        max_part_level_2,\n                    ],\n                    items_localized=True,\n                ).edit()\n                for part_id, level in enumerate(levels):\n                    if part_id == 0:\n                        level -= 1\n                    cannon.levels[part_id] = level\n\n    def get_cannon(self, cannon_id: int) -> Cannon | None:\n        if self.cannons is None:\n            return None\n        return self.cannons.cannons.get(cannon_id, None)\n\n    @staticmethod\n    def get_stage_name(development: int) -> str:\n        if development == 0:\n            return core.core_data.local_manager.get_key(\"none\")\n        if development == 1:\n            return core.core_data.local_manager.get_key(\"foundation\")\n        if development == 2:\n            return core.core_data.local_manager.get_key(\"style\")\n        if development == 3:\n            return core.core_data.local_manager.get_key(\"effect\")\n        return core.core_data.local_manager.get_key(\n            \"unknown_stage\", stage=development\n        )\n\n\ndef edit_cannon(save_file: core.SaveFile):\n    save_file.ototo.edit_cannon(save_file)\n"
  },
  {
    "path": "src/bcsfe/core/game/localizable.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\n\nclass Localizable:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.localizable = self.get_localizable()\n\n    def get_localizable(self) -> dict[str, str] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        data = gdg.download(\"resLocal\", \"localizable.tsv\")\n        if data is None:\n            return None\n        csv = core.CSV(data, \"\\t\")\n        keys: dict[str, str] = {}\n        for line in csv:\n            try:\n                keys[line[0].to_str()] = line[1].to_str()\n            except IndexError:\n                pass\n\n        return keys\n\n    def get(self, key: str) -> str | None:\n        if self.localizable is None:\n            return None\n        return self.localizable.get(key)\n\n    def get_lang(self) -> str | None:\n        return self.get(\"lang\")\n"
  },
  {
    "path": "src/bcsfe/core/game/map/__init__.py",
    "content": "from bcsfe.core.game.map import (\n    story,\n    event,\n    item_reward_stage,\n    timed_score,\n    ex_stage,\n    dojo,\n    outbreaks,\n    tower,\n    challenge,\n    map_reset,\n    uncanny,\n    legend_quest,\n    gauntlets,\n    enigma,\n    aku,\n    zero_legends,\n    chapters,\n    map_names,\n    map_option,\n)\n\n__all__ = [\n    \"story\",\n    \"event\",\n    \"item_reward_stage\",\n    \"timed_score\",\n    \"ex_stage\",\n    \"dojo\",\n    \"outbreaks\",\n    \"tower\",\n    \"challenge\",\n    \"map_reset\",\n    \"uncanny\",\n    \"legend_quest\",\n    \"gauntlets\",\n    \"enigma\",\n    \"aku\",\n    \"zero_legends\",\n    \"chapters\",\n    \"map_names\",\n    \"map_option\",\n]\n"
  },
  {
    "path": "src/bcsfe/core/game/map/aku.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\nclass Stage:\n    def __init__(self, clear_times: int):\n        self.clear_times = clear_times\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read(data: core.Data) -> Stage:\n        clear_times = data.read_short()\n        return Stage(clear_times)\n\n    def write(self, data: core.Data):\n        data.write_short(self.clear_times)\n\n    def serialize(self) -> int:\n        return self.clear_times\n\n    @staticmethod\n    def deserialize(data: int) -> Stage:\n        return Stage(\n            data,\n        )\n\n    def __repr__(self):\n        return f\"Stage({self.clear_times})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def clear_stage(self, clear_count: int = 1):\n        self.clear_times = clear_count\n\n\nclass Chapter:\n    def __init__(self, current_stage: int, total_stages: int = 0):\n        self.current_stage = current_stage\n        self.stages: list[Stage] = [Stage.init() for _ in range(total_stages)]\n\n    @staticmethod\n    def init(total_stages: int) -> Chapter:\n        return Chapter(0, total_stages)\n\n    @staticmethod\n    def read_current_stage(data: core.Data):\n        current_stage = data.read_byte()\n        return Chapter(current_stage)\n\n    def write_current_stage(self, data: core.Data):\n        data.write_byte(self.current_stage)\n\n    def read_stages(self, data: core.Data, total_stages: int):\n        self.stages = [Stage.read(data) for _ in range(total_stages)]\n\n    def write_stages(self, data: core.Data):\n        for stage in self.stages:\n            stage.write(data)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"current_stage\": self.current_stage,\n            \"stages\": [stage.serialize() for stage in self.stages],\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Chapter:\n        chapter = Chapter(data.get(\"current_stage\", 0))\n        chapter.stages = [\n            Stage.deserialize(stage) for stage in data.get(\"stages\", [])\n        ]\n        return chapter\n\n    def __repr__(self):\n        return f\"Chapter({self.current_stage}, {self.stages})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ChaptersStars:\n    def __init__(self, chapters: list[Chapter]):\n        self.chapters = chapters\n\n    @staticmethod\n    def init(total_stages: int, total_stars: int) -> ChaptersStars:\n        return ChaptersStars(\n            [Chapter.init(total_stages) for _ in range(total_stars)]\n        )\n\n    @staticmethod\n    def read_current_stage(data: core.Data, total_stars: int):\n        chapters = [\n            Chapter.read_current_stage(data) for _ in range(total_stars)\n        ]\n        return ChaptersStars(chapters)\n\n    def write_current_stage(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_current_stage(data)\n\n    def read_stages(self, data: core.Data, total_stages: int):\n        for chapter in self.chapters:\n            chapter.read_stages(data, total_stages)\n\n    def write_stages(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_stages(data)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> ChaptersStars:\n        chapters = [Chapter.deserialize(chapter) for chapter in data]\n        return ChaptersStars(chapters)\n\n    def __repr__(self):\n        return f\"ChaptersStars({self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass AkuChapters:\n    def __init__(self, chapters: list[ChaptersStars]):\n        self.chapters = chapters\n\n    @staticmethod\n    def init() -> AkuChapters:\n        return AkuChapters([])\n\n    @staticmethod\n    def read(data: core.Data) -> AkuChapters:\n        total_chapters = data.read_short()\n        total_stages = data.read_byte()\n        total_stars = data.read_byte()\n\n        chapters = [\n            ChaptersStars.read_current_stage(data, total_stars)\n            for _ in range(total_chapters)\n        ]\n\n        for chapter in chapters:\n            chapter.read_stages(data, total_stages)\n\n        return AkuChapters(chapters)\n\n    def write(self, data: core.Data):\n        data.write_short(len(self.chapters))\n        try:\n            data.write_byte(len(self.chapters[0].chapters[0].stages))\n        except IndexError:\n            data.write_byte(0)\n        try:\n            data.write_byte(len(self.chapters[0].chapters))\n        except IndexError:\n            data.write_byte(0)\n\n        for chapter in self.chapters:\n            chapter.write_current_stage(data)\n\n        for chapter in self.chapters:\n            chapter.write_stages(data)\n\n    def serialize(self) -> list[list[dict[str, Any]]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[list[dict[str, Any]]]) -> AkuChapters:\n        chapters = [ChaptersStars.deserialize(chapter) for chapter in data]\n        return AkuChapters(chapters)\n\n    def __repr__(self):\n        return f\"Chapters({self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    @staticmethod\n    def edit_aku_chapters(save_file: core.SaveFile):\n        aku = save_file.aku\n        chapter = aku.chapters[0].chapters[0]\n\n        clear_progress = core.StoryChapters.get_selected_chapter_progress(\n            max_stages=len(chapter.stages)\n        )\n        if clear_progress is None:\n            return\n\n        if clear_progress > 1:\n            individual_clear_count = (\n                core.StoryChapters.ask_if_individual_clear_counts()\n            )\n            if individual_clear_count is None:\n                return\n        else:\n            individual_clear_count = True\n\n        if individual_clear_count:\n            stage_names = core.StageNames(save_file, \"DM\", 49).stage_names\n            if stage_names is None:\n                return\n            for i, stage in enumerate(chapter.stages[:clear_progress]):\n                stage_name = stage_names[i]\n                color.ColoredText.localize(\n                    \"aku_current_stage\", name=stage_name, id=i\n                )\n                clear_count = core.StoryChapters.ask_clear_count()\n                if clear_count is None:\n                    return\n                stage.clear_stage(clear_count)\n        else:\n            clear_count = core.StoryChapters.ask_clear_count()\n            if clear_count is None:\n                return\n            for stage in chapter.stages[:clear_progress]:\n                stage.clear_stage(clear_count)\n\n        for i in range(clear_progress, len(chapter.stages)):\n            chapter.stages[i].clear_stage(clear_count=0)\n\n        color.ColoredText.localize(\"aku_clear_success\")\n"
  },
  {
    "path": "src/bcsfe/core/game/map/challenge.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator\n\n\nclass ChallengeChapters:\n    def __init__(self, chapters: core.Chapters):\n        self.chapters = chapters\n        self.scores: list[int] = []\n        self.shown_popup: bool = False\n\n    @staticmethod\n    def init() -> ChallengeChapters:\n        return ChallengeChapters(core.Chapters.init())\n\n    @staticmethod\n    def read(data: core.Data) -> ChallengeChapters:\n        ch = core.Chapters.read(data)\n        return ChallengeChapters(ch)\n\n    def write(self, data: core.Data):\n        self.chapters.write(data)\n\n    def read_scores(self, data: core.Data):\n        total_scores = data.read_int()\n        self.scores = [data.read_int() for _ in range(total_scores)]\n\n    def write_scores(self, data: core.Data):\n        data.write_int(len(self.scores))\n        for score in self.scores:\n            data.write_int(score)\n\n    def read_popup(self, data: core.Data):\n        self.shown_popup = data.read_bool()\n\n    def write_popup(self, data: core.Data):\n        data.write_bool(self.shown_popup)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": self.chapters.serialize(),\n            \"scores\": self.scores,\n            \"shown_popup\": self.shown_popup,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> ChallengeChapters:\n        challenge = ChallengeChapters(\n            core.Chapters.deserialize(data.get(\"chapters\", {})),\n        )\n        challenge.scores = data.get(\"scores\", [])\n        challenge.shown_popup = data.get(\"shown_popup\", False)\n        return challenge\n\n    def __repr__(self):\n        return f\"Challenge({self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def edit_score(self):\n        if not self.scores:\n            self.scores = [0]\n        self.scores[0] = dialog_creator.SingleEditor(\n            \"challenge_score\", self.scores[0], None, localized_item=True\n        ).edit()\n        self.shown_popup = True\n        self.chapters.clear_stage(0, 0, 0, False)\n\n\ndef edit_challenge_score(save_file: core.SaveFile):\n    save_file.challenge.edit_score()\n"
  },
  {
    "path": "src/bcsfe/core/game/map/chapters.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import edits\n\n\nclass Stage:\n    def __init__(self, clear_times: int):\n        self.clear_times = clear_times\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read(data: core.Data) -> Stage:\n        clear_times = data.read_int()\n        return Stage(clear_times)\n\n    def write(self, data: core.Data):\n        data.write_int(self.clear_times)\n\n    def serialize(self) -> int:\n        return self.clear_times\n\n    @staticmethod\n    def deserialize(data: int) -> Stage:\n        return Stage(\n            data,\n        )\n\n    def __repr__(self):\n        return f\"Stage({self.clear_times})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False):\n        if ensure_cleared_only:\n            self.clear_times = self.clear_times or clear_amount\n        else:\n            self.clear_times = clear_amount\n\n    def unclear_stage(self):\n        self.clear_times = 0\n\n\nclass Chapter:\n    def __init__(self, selected_stage: int, total_stages: int = 0):\n        self.selected_stage = selected_stage\n        self.clear_progress = 0\n        self.stages: list[Stage] = [Stage.init() for _ in range(total_stages)]\n        self.chapter_unlock_state = 0\n\n        self.total_stages = 0\n\n    def clear_stage(\n        self,\n        index: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        if overwrite_clear_progress:\n            self.clear_progress = index + 1\n        else:\n            self.clear_progress = max(self.clear_progress, index + 1)\n        self.chapter_unlock_state = 3\n        self.stages[index].clear_stage(clear_amount, ensure_cleared_only)\n        if index == self.total_stages - 1:\n            return True\n        return False\n\n    def unclear_stage(self, index: int):\n        self.clear_progress = min(self.clear_progress, index)\n        self.stages[index].unclear_stage()\n        return True\n\n    @staticmethod\n    def init(total_stages: int) -> Chapter:\n        return Chapter(0, total_stages)\n\n    @staticmethod\n    def read_selected_stage(data: core.Data) -> Chapter:\n        selected_stage = data.read_int()\n        return Chapter(selected_stage)\n\n    def write_selected_stage(self, data: core.Data):\n        data.write_int(self.selected_stage)\n\n    def read_clear_progress(self, data: core.Data):\n        self.clear_progress = data.read_int()\n\n    def write_clear_progress(self, data: core.Data):\n        data.write_int(self.clear_progress)\n\n    def read_stages(self, data: core.Data, total_stages: int):\n        self.stages = [Stage.read(data) for _ in range(total_stages)]\n\n    def write_stages(self, data: core.Data):\n        for stage in self.stages:\n            stage.write(data)\n\n    def read_chapter_unlock_state(self, data: core.Data):\n        self.chapter_unlock_state = data.read_int()\n\n    def write_chapter_unlock_state(self, data: core.Data):\n        data.write_int(self.chapter_unlock_state)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"selected_stage\": self.selected_stage,\n            \"clear_progress\": self.clear_progress,\n            \"stages\": [stage.serialize() for stage in self.stages],\n            \"chapter_unlock_state\": self.chapter_unlock_state,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Chapter:\n        chapter = Chapter(data.get(\"selected_stage\", 0))\n        chapter.clear_progress = data.get(\"clear_progress\", 0)\n        chapter.stages = [Stage.deserialize(stage) for stage in data.get(\"stages\", [])]\n        chapter.chapter_unlock_state = data.get(\"chapter_unlock_state\", 0)\n        return chapter\n\n    def __repr__(self):\n        return f\"Chapter({self.selected_stage}, {self.clear_progress}, {self.stages}, {self.chapter_unlock_state})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ChaptersStars:\n    def __init__(self, chapters: list[Chapter]):\n        self.chapters = chapters\n\n    def clear_stage(\n        self,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[star].clear_stage(\n            stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished:\n            if star + 1 < len(self.chapters):\n                self.chapters[star + 1].chapter_unlock_state = 1\n        return finished\n\n    def unclear_stage(self, star: int, stage: int):\n        finished = self.chapters[star].unclear_stage(stage)\n        if finished and star + 1 < len(self.chapters):\n            for chapter in self.chapters[star + 1 :]:\n                chapter.chapter_unlock_state = 0\n        return finished\n\n    @staticmethod\n    def init(total_stages: int, total_stars: int) -> ChaptersStars:\n        chapters = [Chapter.init(total_stages) for _ in range(total_stars)]\n        return ChaptersStars(chapters)\n\n    @staticmethod\n    def read_selected_stage(data: core.Data, total_stars: int) -> ChaptersStars:\n        chapters = [Chapter.read_selected_stage(data) for _ in range(total_stars)]\n        return ChaptersStars(chapters)\n\n    def write_selected_stage(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data)\n\n    def read_clear_progress(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.read_clear_progress(data)\n\n    def write_clear_progress(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data)\n\n    def read_stages(self, data: core.Data, total_stages: int):\n        for _ in range(total_stages):\n            for chapter in self.chapters:\n                chapter.stages.append(Stage.read(data))\n\n    def write_stages(self, data: core.Data):\n        for i in range(len(self.chapters[0].stages)):\n            for chapter in self.chapters:\n                chapter.stages[i].write(data)\n\n    def read_chapter_unlock_state(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.read_chapter_unlock_state(data)\n\n    def write_chapter_unlock_state(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> ChaptersStars:\n        chapters = [Chapter.deserialize(chapter) for chapter in data]\n        return ChaptersStars(chapters)\n\n    def __repr__(self):\n        return f\"ChaptersStars({self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass Chapters:\n    def __init__(self, chapters: list[ChaptersStars]):\n        self.chapters = chapters\n\n    def get_total_stars(self, map: int) -> int:\n        return len(self.chapters[map].chapters)\n\n    def get_total_stages(self, map: int, star: int) -> int:\n        return len(self.chapters[map].chapters[star].stages)\n\n    def clear_stage(\n        self,\n        map: int,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[map].clear_stage(\n            star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished and map + 1 < len(self.chapters):\n            self.chapters[map + 1].chapters[0].chapter_unlock_state = 1\n\n        return finished\n\n    def unclear_stage(self, map: int, star: int, stage: int) -> bool:\n        finished = self.chapters[map].unclear_stage(star, stage)\n        if finished and map + 1 < len(self.chapters) and star == 0:\n            for chapter in self.chapters[map + 1].chapters:\n                chapter.chapter_unlock_state = 0\n\n        return finished\n\n    @staticmethod\n    def init() -> Chapters:\n        return Chapters([])\n\n    @staticmethod\n    def read(data: core.Data, read_every_time: bool = True) -> Chapters:\n        total_stages = 0\n        total_chapters = 0\n        total_stars = 0\n        if read_every_time:\n            total_chapters = data.read_int()\n            total_stars = data.read_int()\n        else:\n            total_chapters = data.read_int()\n            total_stages = data.read_int()\n            total_stars = data.read_int()\n\n        chapters = [\n            ChaptersStars.read_selected_stage(data, total_stars)\n            for _ in range(total_chapters)\n        ]\n\n        if read_every_time:\n            total_chapters = data.read_int()\n            total_stars = data.read_int()\n\n        for chapter in chapters:\n            chapter.read_clear_progress(data)\n\n        if read_every_time:\n            total_chapters = data.read_int()\n            total_stages = data.read_int()\n            total_stars = data.read_int()\n\n        for chapter in chapters:\n            chapter.read_stages(data, total_stages)\n\n        if read_every_time:\n            total_chapters = data.read_int()\n            total_stars = data.read_int()\n\n        for chapter in chapters:\n            chapter.read_chapter_unlock_state(data)\n\n        return Chapters(chapters)\n\n    def get_lengths(self) -> tuple[int, int, int]:\n        total_chapters = len(self.chapters)\n        try:\n            total_stages = len(self.chapters[0].chapters[0].stages)\n        except IndexError:\n            total_stages = 0\n\n        try:\n            total_stars = len(self.chapters[0].chapters)\n        except IndexError:\n            total_stars = 0\n        return (total_chapters, total_stages, total_stars)\n\n    def write(self, data: core.Data, write_every_time: bool = True):\n        total_chapters, total_stages, total_stars = self.get_lengths()\n        if write_every_time:\n            data.write_int(total_chapters)\n            data.write_int(total_stars)\n        else:\n            data.write_int(total_chapters)\n            data.write_int(total_stages)\n            data.write_int(total_stars)\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data)\n\n        if write_every_time:\n            data.write_int(total_chapters)\n            data.write_int(total_stars)\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data)\n\n        if write_every_time:\n            data.write_int(total_chapters)\n            data.write_int(total_stages)\n            data.write_int(total_stars)\n        for chapter in self.chapters:\n            chapter.write_stages(data)\n\n        if write_every_time:\n            data.write_int(total_chapters)\n            data.write_int(total_stars)\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data)\n\n    def serialize(self) -> list[list[dict[str, Any]]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[list[dict[str, Any]]]) -> Chapters:\n        chapters = [ChaptersStars.deserialize(chapter) for chapter in data]\n        tower_chapters = Chapters(chapters)\n        return tower_chapters\n\n    def __repr__(self):\n        return f\"Chapters({self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def unclear_rest(self, stages: list[int], stars: int, id: int):\n        if not stages:\n            return\n        for star in range(stars, self.get_total_stars(id)):\n            for stage in range(max(stages), self.get_total_stages(id, star)):\n                self.chapters[id].chapters[star].stages[stage].clear_times = 0\n                self.chapters[id].chapters[star].clear_progress = 0\n\n    def edit_chapters(\n        self, save_file: core.SaveFile, letter_code: str, base_index: int\n    ) -> dict[int, bool] | None:\n        return edits.map.edit_chapters(\n            save_file, self, letter_code, base_index=base_index\n        )\n\n    def set_total_stages(self, map: int, total_stages: int):\n        for chapter in self.chapters[map].chapters:\n            chapter.total_stages = total_stages\n"
  },
  {
    "path": "src/bcsfe/core/game/map/dojo.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator\n\n\nclass Stage:\n    def __init__(self, score: int):\n        self.score = score\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read(stream: core.Data) -> Stage:\n        score = stream.read_int()\n        return Stage(score)\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.score)\n\n    def serialize(self) -> int:\n        return self.score\n\n    @staticmethod\n    def deserialize(data: int) -> Stage:\n        return Stage(data)\n\n    def __repr__(self) -> str:\n        return f\"Stage(score={self.score!r})\"\n\n    def __str__(self) -> str:\n        return f\"Stage(score={self.score!r})\"\n\n\nclass Chapter:\n    def __init__(self, stages: dict[int, Stage]):\n        self.stages = stages\n\n    def get_stage(self, stage_id: int) -> Stage:\n        if stage_id not in self.stages:\n            self.stages[stage_id] = Stage.init()\n        return self.stages[stage_id]\n\n    @staticmethod\n    def init() -> Chapter:\n        return Chapter({})\n\n    @staticmethod\n    def read(stream: core.Data) -> Chapter:\n        total = stream.read_int()\n        stages: dict[int, Stage] = {}\n        for _ in range(total):\n            stage_id = stream.read_int()\n            stage = Stage.read(stream)\n            stages[stage_id] = stage\n\n        return Chapter(stages)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.stages))\n        for stage_id, stage in self.stages.items():\n            stream.write_int(stage_id)\n            stage.write(stream)\n\n    def serialize(self) -> dict[int, Any]:\n        return {stage_id: stage.serialize() for stage_id, stage in self.stages.items()}\n\n    @staticmethod\n    def deserialize(data: dict[int, Any]) -> Chapter:\n        return Chapter(\n            {stage_id: Stage.deserialize(stage) for stage_id, stage in data.items()}\n        )\n\n    def __repr__(self) -> str:\n        return f\"Chapter(stages={self.stages!r})\"\n\n    def __str__(self) -> str:\n        return f\"Chapter(stages={self.stages!r})\"\n\n\nclass Chapters:\n    def __init__(self, chapters: dict[int, Chapter]):\n        self.chapters = chapters\n\n    def get_stage(self, chapter_id: int, stage_id: int) -> Stage:\n        if chapter_id not in self.chapters:\n            self.chapters[chapter_id] = Chapter.init()\n        return self.chapters[chapter_id].get_stage(stage_id)\n\n    @staticmethod\n    def init() -> Chapters:\n        return Chapters({})\n\n    @staticmethod\n    def read(stream: core.Data) -> Chapters:\n        total = stream.read_int()\n        chapters: dict[int, Chapter] = {}\n        for _ in range(total):\n            chapter_id = stream.read_int()\n            chapter = Chapter.read(stream)\n            chapters[chapter_id] = chapter\n\n        return Chapters(chapters)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.chapters))\n        for chapter_id, chapter in self.chapters.items():\n            stream.write_int(chapter_id)\n            chapter.write(stream)\n\n    def serialize(self) -> dict[int, Any]:\n        return {\n            chapter_id: chapter.serialize()\n            for chapter_id, chapter in self.chapters.items()\n        }\n\n    @staticmethod\n    def deserialize(data: dict[int, Any]) -> Chapters:\n        return Chapters(\n            {\n                chapter_id: Chapter.deserialize(chapter)\n                for chapter_id, chapter in data.items()\n            }\n        )\n\n    def __repr__(self) -> str:\n        return f\"Chapters(chapters={self.chapters!r})\"\n\n    def __str__(self) -> str:\n        return f\"Chapters(chapters={self.chapters!r})\"\n\n\nclass Ranking:\n    def __init__(\n        self,\n        score: int,\n        ranking: int,\n        has_submitted: bool,\n        has_completed: bool,\n        has_seen_results: bool,\n        start_date: int,\n        end_date: int,\n        event_number: int,\n        should_show_rank_description: bool,\n        should_show_start_message: bool,\n        submit_error_flag: bool,\n        other: str | None,\n    ):\n        self.score = score\n        self.ranking = ranking\n        self.has_submitted = has_submitted\n        self.has_completed = has_completed\n        self.has_seen_results = has_seen_results\n        self.start_date = start_date\n        self.end_date = end_date\n        self.event_number = event_number\n        self.should_show_rank_description = should_show_rank_description\n        self.should_show_start_message = should_show_start_message\n        self.submit_error_flag = submit_error_flag\n        self.did_win_rewards = False\n        self.other = other\n\n    @staticmethod\n    def init() -> Ranking:\n        return Ranking(\n            0,\n            0,\n            False,\n            False,\n            False,\n            0,\n            0,\n            0,\n            False,\n            False,\n            False,\n            None,\n        )\n\n    @staticmethod\n    def read(stream: core.Data, game_version: core.GameVersion) -> Ranking:\n        score = stream.read_int()\n        ranking = stream.read_int()\n        has_submitted = stream.read_bool()\n        has_completed = stream.read_bool()\n        has_seen_results = stream.read_bool()\n        start_date = stream.read_int()\n        end_date = stream.read_int()\n        event_number = stream.read_int()\n        should_show_rank_description = stream.read_bool()\n        should_show_start_message = stream.read_bool()\n        submit_error_flag = stream.read_bool()\n\n        if game_version >= 140500:\n            # game seems to do more that just this, may break in the future\n            other = stream.read_string()\n        else:\n            other = None\n        return Ranking(\n            score,\n            ranking,\n            has_submitted,\n            has_completed,\n            has_seen_results,\n            start_date,\n            end_date,\n            event_number,\n            should_show_rank_description,\n            should_show_start_message,\n            submit_error_flag,\n            other,\n        )\n\n    def write(self, stream: core.Data, game_version: core.GameVersion):\n        stream.write_int(self.score)\n        stream.write_int(self.ranking)\n        stream.write_bool(self.has_submitted)\n        stream.write_bool(self.has_completed)\n        stream.write_bool(self.has_seen_results)\n        stream.write_int(self.start_date)\n        stream.write_int(self.end_date)\n        stream.write_int(self.event_number)\n        stream.write_bool(self.should_show_rank_description)\n        stream.write_bool(self.should_show_start_message)\n        stream.write_bool(self.submit_error_flag)\n        if game_version >= 140500:\n            # game seems to do more that just this, may break in the future\n            stream.write_string(self.other or \"\")\n\n    def read_did_win_rewards(self, stream: core.Data):\n        self.did_win_rewards = stream.read_bool()\n\n    def write_did_win_rewards(self, stream: core.Data):\n        stream.write_bool(self.did_win_rewards)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"score\": self.score,\n            \"ranking\": self.ranking,\n            \"has_submitted\": self.has_submitted,\n            \"has_completed\": self.has_completed,\n            \"has_seen_results\": self.has_seen_results,\n            \"start_date\": self.start_date,\n            \"end_date\": self.end_date,\n            \"event_number\": self.event_number,\n            \"should_show_rank_description\": self.should_show_rank_description,\n            \"should_show_start_message\": self.should_show_start_message,\n            \"submit_error_flag\": self.submit_error_flag,\n            \"did_win_rewards\": self.did_win_rewards,\n            \"other\": self.other,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Ranking:\n        ranking = Ranking(\n            data.get(\"score\", 0),\n            data.get(\"ranking\", 0),\n            data.get(\"has_submitted\", False),\n            data.get(\"has_completed\", False),\n            data.get(\"has_seen_results\", False),\n            data.get(\"start_date\", 0),\n            data.get(\"end_date\", 0),\n            data.get(\"event_number\", 0),\n            data.get(\"should_show_rank_description\", False),\n            data.get(\"should_show_start_message\", False),\n            data.get(\"submit_error_flag\", False),\n            data.get(\"other\", None),\n        )\n        ranking.did_win_rewards = data.get(\"did_win_rewards\", False)\n        return ranking\n\n    def __repr__(self) -> str:\n        return (\n            f\"Ranking(score={self.score!r}, ranking={self.ranking!r}, \"\n            f\"has_submitted={self.has_submitted!r}, has_completed={self.has_completed!r}, \"\n            f\"has_seen_results={self.has_seen_results!r}, start_date={self.start_date!r}, \"\n            f\"end_date={self.end_date!r}, event_number={self.event_number!r}, \"\n            f\"should_show_rank_description={self.should_show_rank_description!r}, \"\n            f\"should_show_start_message={self.should_show_start_message!r}, \"\n            f\"submit_error_flag={self.submit_error_flag!r},\"\n            f\"did_win_rewards={self.did_win_rewards!r}),\"\n            f\"other={self.other!r})\"\n        )\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass Dojo:\n    def __init__(self, chapters: Chapters):\n        self.chapters = chapters\n        self.item_lock_flags = False\n        self.item_locks = [False] * 6\n        self.ranking = Ranking.init()\n\n    @staticmethod\n    def init() -> Dojo:\n        return Dojo(Chapters.init())\n\n    @staticmethod\n    def read_chapters(stream: core.Data) -> Dojo:\n        chapters = Chapters.read(stream)\n        return Dojo(chapters)\n\n    def write_chapters(self, stream: core.Data):\n        self.chapters.write(stream)\n\n    def read_item_locks(self, stream: core.Data):\n        self.item_lock_flags = stream.read_bool()\n        self.item_locks = stream.read_bool_list(6)\n\n    def write_item_locks(self, stream: core.Data):\n        stream.write_bool(self.item_lock_flags)\n        stream.write_bool_list(self.item_locks, write_length=False, length=6)\n\n    def read_ranking(self, stream: core.Data, game_version: core.GameVersion):\n        self.ranking = Ranking.read(stream, game_version)\n\n    def write_ranking(self, stream: core.Data, game_version: core.GameVersion):\n        self.ranking.write(stream, game_version)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": self.chapters.serialize(),\n            \"item_locks\": self.item_locks,\n            \"item_lock_flags\": self.item_lock_flags,\n            \"ranking\": self.ranking.serialize(),\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Dojo:\n        chapters = Chapters.deserialize(data.get(\"chapters\", {}))\n        item_locks = data.get(\"item_locks\", [])\n        item_lock_flags = data.get(\"item_lock_flags\", False)\n        dojo = Dojo(chapters)\n        dojo.item_locks = item_locks\n        dojo.item_lock_flags = item_lock_flags\n        dojo.ranking = Ranking.deserialize(data.get(\"ranking\", {}))\n        return dojo\n\n    def __repr__(self) -> str:\n        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})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def edit_score(self):\n        stage = self.chapters.get_stage(0, 0)\n        stage.score = dialog_creator.SingleEditor(\n            \"dojo_score\",\n            stage.score,\n            None,\n            localized_item=True,\n        ).edit()\n\n\ndef edit_dojo_score(save_file: core.SaveFile):\n    save_file.dojo.edit_score()\n"
  },
  {
    "path": "src/bcsfe/core/game/map/enigma.py",
    "content": "from __future__ import annotations\nimport time\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass Stage:\n    def __init__(\n        self,\n        level: int,\n        stage_id: int,\n        decoding_satus: int,\n        start_time: float,\n    ):\n        self.level = level\n        self.stage_id = stage_id\n        self.decoding_satus = decoding_satus\n        self.start_time = start_time\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0, 0, 0, 0.0)\n\n    @staticmethod\n    def read(data: core.Data) -> Stage:\n        level = data.read_int()\n        stage_id = data.read_int()\n        decoding_satus = data.read_byte()\n        start_time = data.read_double()\n\n        return Stage(level, stage_id, decoding_satus, start_time)\n\n    def write(self, data: core.Data):\n        data.write_int(self.level)\n        data.write_int(self.stage_id)\n        data.write_byte(self.decoding_satus)\n        data.write_double(self.start_time)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"level\": self.level,\n            \"stage_id\": self.stage_id,\n            \"decoding_satus\": self.decoding_satus,\n            \"start_time\": self.start_time,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Stage:\n        return Stage(\n            data.get(\"level\", 0),\n            data.get(\"stage_id\", 0),\n            data.get(\"decoding_satus\", 0),\n            data.get(\"start_time\", 0.0),\n        )\n\n    def __repr__(self):\n        return f\"Stage({self.level}, {self.stage_id}, {self.decoding_satus}, {self.start_time})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass Enigma:\n    def __init__(\n        self,\n        energy_since_1: int,\n        energy_since_2: int,\n        enigma_level: int,\n        unknown_1: int,\n        unknown_2: bool,\n        stages: list[Stage],\n        extra: tuple[int, int, int, float] | None,\n    ):\n        self.energy_since_1 = energy_since_1\n        self.energy_since_2 = energy_since_2\n        self.enigma_level = enigma_level\n        self.unknown_1 = unknown_1\n        self.unknown_2 = unknown_2\n        self.stages = stages\n        self.extra = extra\n\n    @staticmethod\n    def init() -> Enigma:\n        return Enigma(0, 0, 0, 0, False, [], None)\n\n    @staticmethod\n    def read(data: core.Data, game_version: core.GameVersion) -> Enigma:\n        energy_since_1 = data.read_int()\n        energy_since_2 = data.read_int()\n        enigma_level = data.read_byte()\n        unknown_1 = data.read_byte()\n        unknown_2 = data.read_bool()\n\n        total_stages = data.read_byte()\n\n        stages = [Stage.read(data) for _ in range(total_stages)]\n\n        extra_data = None\n\n        if game_version >= 140500:\n            has_extra = data.read_bool()\n            if has_extra:\n                extra_data = (\n                    data.read_int(),\n                    data.read_int(),\n                    data.read_byte(),\n                    data.read_double(),\n                )\n        return Enigma(\n            energy_since_1,\n            energy_since_2,\n            enigma_level,\n            unknown_1,\n            unknown_2,\n            stages,\n            extra_data,\n        )\n\n    def write(self, data: core.Data, game_version: core.GameVersion):\n        data.write_int(self.energy_since_1)\n        data.write_int(self.energy_since_2)\n        data.write_byte(self.enigma_level)\n        data.write_byte(self.unknown_1)\n        data.write_bool(self.unknown_2)\n        data.write_byte(len(self.stages))\n        for stage in self.stages:\n            stage.write(data)\n\n        if game_version >= 140500:\n            data.write_bool(self.extra is not None)\n            if self.extra is not None:\n                data.write_int(self.extra[0])\n                data.write_int(self.extra[1])\n                data.write_byte(self.extra[2])\n                data.write_double(self.extra[3])\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"energy_since_1\": self.energy_since_1,\n            \"energy_since_2\": self.energy_since_2,\n            \"enigma_level\": self.enigma_level,\n            \"unknown_1\": self.unknown_1,\n            \"unknown_2\": self.unknown_2,\n            \"stages\": [stage.serialize() for stage in self.stages],\n            \"extra\": self.extra,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Enigma:\n        return Enigma(\n            data.get(\"energy_since_1\", 0),\n            data.get(\"energy_since_2\", 0),\n            data.get(\"enigma_level\", 0),\n            data.get(\"unknown_1\", 0),\n            data.get(\"unknown_2\", False),\n            [Stage.deserialize(stage) for stage in data.get(\"stages\", [])],\n            data.get(\"extra\", None),\n        )\n\n    def __repr__(self):\n        return f\"Enigma({self.energy_since_1}, {self.energy_since_2}, {self.enigma_level}, {self.unknown_1}, {self.unknown_2}, {self.stages}, {self.extra})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def edit_enigma(self, save_file: core.SaveFile):\n        names = core.MapNames(save_file, \"H\", base_index=25000).map_names\n        names_list: list[str] = []\n        keys = list(names.keys())\n        keys.sort()\n        for id in keys:\n            name = names[id]\n            if name is None:\n                name = core.core_data.local_manager.get_key(\n                    \"unknown_enigma_name\", id=id\n                )\n            names_list.append(name)\n\n        base_level = 25000\n\n        color.ColoredText.localize(\"current_enigma_stages\")\n        for stage in self.stages:\n            name = names[stage.stage_id - base_level]\n            if name is None:\n                name = core.core_data.local_manager.get_key(\n                    \"unknown_enigma_name\", id=stage.stage_id\n                )\n            color.ColoredText.localize(\n                \"enigma_stage\", name=name, id=stage.stage_id - base_level\n            )\n\n        if self.stages:\n            wipe = dialog_creator.YesNoInput().get_input_once(\"wipe_enigma\")\n            if wipe is None:\n                return\n            if wipe:\n                for stage in self.stages:\n                    id = stage.stage_id\n                    save_file.event_stages.chapter_completion_count[id] = 0\n                self.stages = []\n\n        ids, _ = dialog_creator.ChoiceInput(\n            names_list,\n            names_list,\n            [],\n            {},\n            \"enigma_select\",\n        ).multiple_choice()\n        if ids is None:\n            return\n\n        for enigma_id in ids:\n            abs_id = enigma_id + base_level\n            save_file.event_stages.chapter_completion_count[abs_id] = 0\n            # TODO: level? they can go much higher than 3... not sure it really matters though\n            stage = Stage(3, abs_id, 2, int(time.time()))\n            self.stages.append(stage)\n\n        color.ColoredText.localize(\"enigma_success\")\n\n\ndef edit_enigma(save_file: core.SaveFile):\n    save_file.enigma.edit_enigma(save_file)\n"
  },
  {
    "path": "src/bcsfe/core/game/map/event.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator, edits\n\n\nclass EventStage:\n    def __init__(self, clear_amount: int):\n        self.clear_amount = clear_amount\n\n    @staticmethod\n    def init() -> EventStage:\n        return EventStage(0)\n\n    @staticmethod\n    def read(data: core.Data, is_int: bool) -> EventStage:\n        if is_int:\n            clear_amount = data.read_int()\n        else:\n            clear_amount = data.read_short()\n        return EventStage(clear_amount)\n\n    def write(self, data: core.Data, is_int: bool):\n        if is_int:\n            data.write_int(self.clear_amount)\n        else:\n            data.write_short(self.clear_amount)\n\n    def serialize(self) -> int:\n        return self.clear_amount\n\n    @staticmethod\n    def deserialize(data: int) -> EventStage:\n        return EventStage(\n            clear_amount=data,\n        )\n\n    def __repr__(self) -> str:\n        return f\"<EventStage clear_amount={self.clear_amount}>\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False):\n        if ensure_cleared_only:\n            self.clear_amount = self.clear_amount or clear_amount\n        else:\n            self.clear_amount = clear_amount\n\n    def unclear_stage(self):\n        self.clear_amount = 0\n\n\nclass EventSubChapter:\n    def __init__(self, selected_stage: int, total_stages: int = 0):\n        self.selected_stage = selected_stage\n        self.clear_progress = 0\n        self.stages = [EventStage.init() for _ in range(total_stages)]\n        self.chapter_unlock_state = 0\n\n    def clear_stage(\n        self,\n        index: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        if overwrite_clear_progress:\n            self.clear_progress = index + 1\n        else:\n            self.clear_progress = max(self.clear_progress, index + 1)\n        self.stages[index].clear_stage(clear_amount, ensure_cleared_only)\n        self.chapter_unlock_state = 3\n        if index == len(self.stages) - 1:\n            return True\n        return False\n\n    def unclear_stage(self, index: int) -> bool:\n        self.clear_progress = min(self.clear_progress, index)\n        self.stages[index].unclear_stage()\n\n        return True\n\n    def clear_map(self, increment: bool = True) -> bool:\n        self.clear_progress = len(self.stages)\n        self.chapter_unlock_state = 3\n        for stage in self.stages:\n            if increment:\n                clear_amount = stage.clear_amount + 1\n            else:\n                clear_amount = stage.clear_amount or 1\n            stage.clear_stage(clear_amount)\n        return True\n\n    @staticmethod\n    def init(total_stages: int) -> EventSubChapter:\n        return EventSubChapter(0, total_stages)\n\n    @staticmethod\n    def read_selected_stage(data: core.Data, is_int: bool) -> EventSubChapter:\n        if is_int:\n            selected_stage = data.read_int()\n        else:\n            selected_stage = data.read_byte()\n        return EventSubChapter(selected_stage)\n\n    def write_selected_stage(self, data: core.Data, is_int: bool):\n        if is_int:\n            data.write_int(self.selected_stage)\n        else:\n            data.write_byte(self.selected_stage)\n\n    def read_clear_progress(self, data: core.Data, is_int: bool):\n        if is_int:\n            self.clear_progress = data.read_int()\n        else:\n            self.clear_progress = data.read_byte()\n\n    def write_clear_progress(self, data: core.Data, is_int: bool):\n        if is_int:\n            data.write_int(self.clear_progress)\n        else:\n            data.write_byte(self.clear_progress)\n\n    def read_stages(self, data: core.Data, total_stages: int, is_int: bool):\n        self.stages = [EventStage.read(data, is_int) for _ in range(total_stages)]\n\n    def write_stages(self, data: core.Data, is_int: bool):\n        for stage in self.stages:\n            stage.write(data, is_int)\n\n    def read_chapter_unlock_state(self, data: core.Data, is_int: bool):\n        if is_int:\n            self.chapter_unlock_state = data.read_int()\n        else:\n            self.chapter_unlock_state = data.read_byte()\n\n    def write_chapter_unlock_state(self, data: core.Data, is_int: bool):\n        if is_int:\n            data.write_int(self.chapter_unlock_state)\n        else:\n            data.write_byte(self.chapter_unlock_state)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"selected_stage\": self.selected_stage,\n            \"clear_progress\": self.clear_progress,\n            \"stages\": [stage.serialize() for stage in self.stages],\n            \"chapter_unlock_state\": self.chapter_unlock_state,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> EventSubChapter:\n        sub_chapter = EventSubChapter(\n            selected_stage=data.get(\"selected_stage\", 0),\n        )\n        sub_chapter.clear_progress = data.get(\"clear_progress\", 0)\n        sub_chapter.stages = [\n            EventStage.deserialize(stage) for stage in data.get(\"stages\", [])\n        ]\n        sub_chapter.chapter_unlock_state = data.get(\"chapter_unlock_state\", 0)\n        return sub_chapter\n\n    def __repr__(self) -> str:\n        return f\"<EventSubChapter selected_stage={self.selected_stage}, clear_progress={self.clear_progress}, stages={self.stages}, chapter_unlock_state={self.chapter_unlock_state}>\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass EventSubChapterStars:\n    def __init__(self, chapters: list[EventSubChapter]):\n        self.chapters = chapters\n        self.legend_restriction = 0\n\n    def clear_stage(\n        self,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[star].clear_stage(\n            stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished:\n            if star + 1 < len(self.chapters):\n                self.chapters[star + 1].chapter_unlock_state = 1\n        return finished\n\n    def unclear_stage(self, star: int, stage: int):\n        finished = self.chapters[star].unclear_stage(stage)\n        if finished and star + 1 < len(self.chapters):\n            for chapter in self.chapters[star + 1 :]:\n                chapter.chapter_unlock_state = 0\n        return finished\n\n    def clear_map(self, star: int, increment: bool = True) -> bool:\n        finished = self.chapters[star].clear_map(increment)\n        if finished:\n            if star + 1 < len(self.chapters):\n                self.chapters[star + 1].chapter_unlock_state = 1\n        return finished\n\n    def clear_chapter(self, increment: bool = True) -> bool:\n        for chapter in self.chapters:\n            chapter.clear_map(increment)\n        return True\n\n    @staticmethod\n    def init(total_stars: int) -> EventSubChapterStars:\n        return EventSubChapterStars(\n            [EventSubChapter.init(0) for _ in range(total_stars)]\n        )\n\n    @staticmethod\n    def read_selected_stage(\n        data: core.Data, total_stars: int, is_int: bool\n    ) -> EventSubChapterStars:\n        chapters = [\n            EventSubChapter.read_selected_stage(data, is_int)\n            for _ in range(total_stars)\n        ]\n        return EventSubChapterStars(chapters)\n\n    def write_selected_stage(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data, is_int)\n\n    def read_clear_progress(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.read_clear_progress(data, is_int)\n\n    def write_clear_progress(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data, is_int)\n\n    def read_stages(self, data: core.Data, total_stages: int, is_int: bool):\n        for _ in range(total_stages):\n            for chapter in self.chapters:\n                chapter.stages.append(EventStage.read(data, is_int))\n                # chapter.read_stages(data, total_stages, is_int)\n\n    def write_stages(self, data: core.Data, is_int: bool):\n        for i in range(len(self.chapters[0].stages)):\n            for chapter in self.chapters:\n                chapter.stages[i].write(data, is_int)\n                # chapter.write_stages(data, is_int)\n\n    def read_chapter_unlock_state(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.read_chapter_unlock_state(data, is_int)\n\n    def write_chapter_unlock_state(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data, is_int)\n\n    def read_legend_restrictions(self, data: core.Data):\n        self.legend_restriction = data.read_int()\n\n    def write_legend_restrictions(self, data: core.Data):\n        data.write_int(self.legend_restriction)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": [chapter.serialize() for chapter in self.chapters],\n            \"legend_restriction\": self.legend_restriction,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> EventSubChapterStars:\n        chapters = [\n            EventSubChapter.deserialize(chapter) for chapter in data.get(\"chapters\", [])\n        ]\n        chapter = EventSubChapterStars(chapters)\n        chapter.legend_restriction = data.get(\"legend_restriction\", 0)\n        return chapter\n\n    def __repr__(self) -> str:\n        return f\"<EventSubChapterStars chapters={self.chapters}, legend_restriction={self.legend_restriction}>\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass EventChapterGroup:\n    def __init__(self, chapters: list[EventSubChapterStars]):\n        self.chapters = chapters\n\n    def clear_stage(\n        self,\n        map: int,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[map].clear_stage(\n            star,\n            stage,\n            clear_amount,\n            overwrite_clear_progress,\n            ensure_cleared_only,\n        )\n        if finished and map + 1 < len(self.chapters):\n            self.chapters[map + 1].chapters[0].chapter_unlock_state = 1\n\n        return finished\n\n    def unclear_stage(self, map: int, star: int, stage: int) -> bool:\n        finished = self.chapters[map].unclear_stage(star, stage)\n        if finished and map + 1 < len(self.chapters) and star == 0:\n            for chapter in self.chapters[map + 1].chapters:\n                chapter.chapter_unlock_state = 0\n\n        return finished\n\n    def clear_map(self, map: int, star: int, increment: bool = True):\n        finished = self.chapters[map].clear_map(star, increment)\n        if finished and map + 1 < len(self.chapters):\n            self.chapters[map + 1].chapters[0].chapter_unlock_state = 1\n\n    def clear_chapter(self, map: int, increment: bool = True):\n        finished = self.chapters[map].clear_chapter(increment)\n        if finished and map + 1 < len(self.chapters):\n            self.chapters[map + 1].chapters[0].chapter_unlock_state = 1\n\n    def clear_group(self, increment: bool = True):\n        for chapter in self.chapters:\n            chapter.clear_chapter(increment)\n\n    @staticmethod\n    def init(total_subchapters: int, total_stars: int) -> EventChapterGroup:\n        return EventChapterGroup(\n            [EventSubChapterStars.init(total_stars) for _ in range(total_subchapters)]\n        )\n\n    @staticmethod\n    def read_selected_stage(\n        data: core.Data, total_subchapters: int, total_stars: int, is_int: bool\n    ) -> EventChapterGroup:\n        chapters = [\n            EventSubChapterStars.read_selected_stage(data, total_stars, is_int)\n            for _ in range(total_subchapters)\n        ]\n        return EventChapterGroup(chapters)\n\n    def write_selected_stage(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data, is_int)\n\n    def read_clear_progress(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.read_clear_progress(data, is_int)\n\n    def write_clear_progress(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data, is_int)\n\n    def read_stages(self, data: core.Data, total_stages: int, is_int: bool):\n        for chapter in self.chapters:\n            chapter.read_stages(data, total_stages, is_int)\n\n    def write_stages(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.write_stages(data, is_int)\n\n    def read_chapter_unlock_state(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.read_chapter_unlock_state(data, is_int)\n\n    def write_chapter_unlock_state(self, data: core.Data, is_int: bool):\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data, is_int)\n\n    def read_legend_restrictions(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.read_legend_restrictions(data)\n\n    def write_legend_restrictions(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_legend_restrictions(data)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> EventChapterGroup:\n        chapters = [EventSubChapterStars.deserialize(chapter) for chapter in data]\n        return EventChapterGroup(chapters)\n\n    def __repr__(self) -> str:\n        return f\"<EventChapterGroup chapters={self.chapters}>\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass EventChapters:\n    def __init__(self, chapters: list[EventChapterGroup]):\n        self.chapters = chapters\n        self.chapter_completion_count: dict[int, int] = {}\n        self.displayed_cleared_limit_text: dict[int, bool] = {}\n        self.event_start_dates: dict[int, int] = {}\n        self.stages_reward_claimed: list[int] = []\n\n    def clear_stage(\n        self,\n        type: int,\n        map: int,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        return self.chapters[type].clear_stage(\n            map,\n            star,\n            stage,\n            clear_amount,\n            overwrite_clear_progress,\n            ensure_cleared_only,\n        )\n\n    def unclear_stage(self, type: int, map: int, star: int, stage: int) -> bool:\n        return self.chapters[type].unclear_stage(map, star, stage)\n\n    def clear_map(self, type: int, map: int, star: int, increment: bool = True):\n        self.chapters[type].clear_map(map, star, increment)\n\n    def clear_chapter(self, type: int, map: int, increment: bool = True):\n        self.chapters[type].clear_chapter(map, increment)\n\n    def clear_group(self, type: int, increment: bool = True):\n        self.chapters[type].clear_group(increment)\n\n    @staticmethod\n    def init(gv: core.GameVersion) -> EventChapters:\n        if gv < 20:\n            return EventChapters([])\n        if gv <= 32:\n            total_map_types = 3\n            total_subchapters = 150\n            stars_per_subchapter = 3\n        elif gv <= 34:\n            total_map_types = 4\n            total_subchapters = 150\n            stars_per_subchapter = 3\n        else:\n            total_map_types = 0\n            total_subchapters = 0\n            stars_per_subchapter = 0\n\n        return EventChapters(\n            [\n                EventChapterGroup.init(total_subchapters, stars_per_subchapter)\n                for _ in range(total_map_types)\n            ]\n        )\n\n    @staticmethod\n    def read(data: core.Data, gv: core.GameVersion) -> EventChapters:\n        if gv < 20:\n            return EventChapters([])\n        stages_per_subchapter = 0\n        if 80099 < gv:\n            total_map_types = data.read_byte()\n            total_subchapters = data.read_short()\n            stars_per_subchapter = data.read_byte()\n            stages_per_subchapter = data.read_byte()\n            is_int = False\n        elif gv <= 32:\n            total_map_types = 3\n            total_subchapters = 150\n            stars_per_subchapter = 3\n            is_int = True\n        elif gv <= 34:\n            total_map_types = 4\n            total_subchapters = 150\n            stars_per_subchapter = 3\n            is_int = True\n        else:\n            total_map_types = data.read_int()\n            total_subchapters = data.read_int()\n            stars_per_subchapter = data.read_int()\n            is_int = True\n        chapters = [\n            EventChapterGroup.read_selected_stage(\n                data, total_subchapters, stars_per_subchapter, is_int\n            )\n            for _ in range(total_map_types)\n        ]\n        if 80099 < gv:\n            is_int = False\n        elif gv <= 32:\n            total_map_types = 3\n            total_subchapters = 150\n            stars_per_subchapter = 3\n            is_int = True\n        elif gv <= 34:\n            total_map_types = 4\n            total_subchapters = 150\n            stars_per_subchapter = 3\n            is_int = True\n        else:\n            total_map_types = data.read_int()\n            total_subchapters = data.read_int()\n            stars_per_subchapter = data.read_int()\n            is_int = True\n\n        for chapter in chapters:\n            chapter.read_clear_progress(data, is_int)\n\n        if 80099 < gv:\n            is_int = False\n        elif gv <= 32:\n            total_map_types = 3\n            total_subchapters = 150\n            stars_per_subchapter = 3\n            stages_per_subchapter = 12\n            is_int = True\n        elif gv <= 34:\n            total_map_types = 4\n            total_subchapters = 150\n            stars_per_subchapter = 3\n            stages_per_subchapter = 12\n            is_int = True\n        else:\n            total_map_types = data.read_int()\n            total_subchapters = data.read_int()\n            stages_per_subchapter = data.read_int()\n            stars_per_subchapter = data.read_int()\n            is_int = True\n\n        for chapter in chapters:\n            chapter.read_stages(data, stages_per_subchapter, is_int)\n\n        if 80099 < gv:\n            is_int = False\n        elif gv <= 32:\n            total_map_types = 3\n            total_subchapters = 150\n            stars_per_subchapter = 3\n            is_int = True\n        elif gv <= 34:\n            total_map_types = 4\n            total_subchapters = 150\n            stars_per_subchapter = 3\n            is_int = True\n        else:\n            total_map_types = data.read_int()\n            total_subchapters = data.read_int()\n            stars_per_subchapter = data.read_int()\n            is_int = True\n\n        for chapter in chapters:\n            chapter.read_chapter_unlock_state(data, is_int)\n\n        return EventChapters(chapters)\n\n    def get_lengths(self) -> tuple[int, int, int, int]:\n        total_map_types = len(self.chapters)\n        try:\n            total_subchapters = len(self.chapters[0].chapters)\n        except IndexError:\n            total_subchapters = 0\n\n        try:\n            stars_per_subchapter = len(self.chapters[0].chapters[0].chapters)\n        except IndexError:\n            stars_per_subchapter = 0\n\n        try:\n            stages_per_subchapter = len(self.chapters[0].chapters[0].chapters[0].stages)\n        except IndexError:\n            stages_per_subchapter = 0\n        return (\n            total_map_types,\n            total_subchapters,\n            stars_per_subchapter,\n            stages_per_subchapter,\n        )\n\n    def write(self, data: core.Data, gv: core.GameVersion):\n        (\n            total_map_types,\n            total_subchapters,\n            stars_per_subchapter,\n            stages_per_subchapter,\n        ) = self.get_lengths()\n        if gv <= 34:\n            is_int = True\n        else:\n            if 80099 < gv:\n                data.write_byte(total_map_types)\n                data.write_short(total_subchapters)\n                data.write_byte(stars_per_subchapter)\n                data.write_byte(stages_per_subchapter)\n                is_int = False\n            else:\n                data.write_int(total_map_types)\n                data.write_int(total_subchapters)\n                data.write_int(stars_per_subchapter)\n                is_int = True\n\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data, is_int)\n\n        if gv <= 34:\n            is_int = True\n        else:\n            if 80099 < gv:\n                is_int = False\n            else:\n                data.write_int(total_map_types)\n                data.write_int(total_subchapters)\n                data.write_int(stars_per_subchapter)\n                is_int = True\n\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data, is_int)\n\n        if gv <= 34:\n            is_int = True\n        else:\n            if 80099 < gv:\n                is_int = False\n            else:\n                data.write_int(total_map_types)\n                data.write_int(total_subchapters)\n                data.write_int(stages_per_subchapter)\n                data.write_int(stars_per_subchapter)\n                is_int = True\n\n        for chapter in self.chapters:\n            chapter.write_stages(data, is_int)\n\n        if gv <= 34:\n            is_int = True\n        else:\n            if 80099 < gv:\n                is_int = False\n            else:\n                data.write_int(total_map_types)\n                data.write_int(total_subchapters)\n                data.write_int(stars_per_subchapter)\n                is_int = True\n\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data, is_int)\n\n    def read_legend_restrictions(self, data: core.Data, gv: core.GameVersion):\n        if gv < 20:\n            return\n        if gv < 33:\n            total_map_types = 3  # type: ignore\n            total_subchapters = 150  # type: ignore\n        elif gv < 41:\n            total_map_types = 4  # type: ignore\n            total_subchapters = 150  # type: ignore\n        else:\n            total_map_types = data.read_int()  # type: ignore\n            total_subchapters = data.read_int()  # type: ignore\n\n        for chapter in self.chapters:\n            chapter.read_legend_restrictions(data)\n\n    def write_legend_restrictions(self, data: core.Data, gv: core.GameVersion):\n        if gv < 20:\n            return\n        if gv >= 41:\n            data.write_int(len(self.chapters))\n            try:\n                data.write_int(len(self.chapters[0].chapters))\n            except IndexError:\n                data.write_int(0)\n\n        for chapter in self.chapters:\n            chapter.write_legend_restrictions(data)\n\n    def read_dicts(self, data: core.Data):\n        self.chapter_completion_count = data.read_int_int_dict()\n        self.displayed_cleared_limit_text = data.read_int_bool_dict()\n        self.event_start_dates = data.read_int_int_dict()\n        self.stages_reward_claimed = data.read_int_list()\n\n    def write_dicts(self, data: core.Data):\n        data.write_int_int_dict(self.chapter_completion_count)\n        data.write_int_bool_dict(self.displayed_cleared_limit_text)\n        data.write_int_int_dict(self.event_start_dates)\n        data.write_int_list(self.stages_reward_claimed)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": [chapter.serialize() for chapter in self.chapters],\n            \"chapter_completion_count\": self.chapter_completion_count,\n            \"displayed_cleared_limit_text\": self.displayed_cleared_limit_text,\n            \"event_start_dates\": self.event_start_dates,\n            \"stages_reward_claimed\": self.stages_reward_claimed,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> EventChapters:\n        chapters = [\n            EventChapterGroup.deserialize(chapter)\n            for chapter in data.get(\"chapters\", [])\n        ]\n        ch = EventChapters(chapters)\n        ch.chapter_completion_count = data.get(\"chapter_completion_count\", {})\n        ch.displayed_cleared_limit_text = data.get(\"displayed_cleared_limit_text\", {})\n        ch.event_start_dates = data.get(\"event_start_dates\", {})\n        ch.stages_reward_claimed = data.get(\"stages_reward_claimed\", [])\n        return ch\n\n    def __repr__(self) -> str:\n        return f\"EventChapters({self.chapters}, {self.chapter_completion_count}, {self.displayed_cleared_limit_text}, {self.event_start_dates}, {self.stages_reward_claimed})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def get_total_stars(self, type: int, map: int) -> int:\n        try:\n            return len(self.chapters[type].chapters[map].chapters)\n        except IndexError:\n            return len(self.chapters[0].chapters[0].chapters)\n\n    def get_total_stages(self, type: int, map: int, star: int) -> int:\n        try:\n            return len(self.chapters[type].chapters[map].chapters[star].stages)\n        except IndexError:\n            return len(self.chapters[0].chapters[0].chapters[0].stages)\n\n    @staticmethod\n    def ask_stars(\n        max_stars: int, prompt: str = \"custom_star_count_per_chapter\"\n    ) -> int | None:\n        if max_stars <= 1:\n            return max_stars\n        stars = dialog_creator.IntInput(min=1, max=max_stars).get_input_locale(\n            prompt, {\"max\": max_stars}\n        )[0]\n        if stars is None:\n            return None\n        return stars\n\n    @staticmethod\n    def ask_stars_unclear(\n        max_stars: int, prompt: str = \"custom_star_count_per_chapter\"\n    ) -> int | None:\n        stars = dialog_creator.IntInput(min=0, max=max_stars).get_input_locale(\n            prompt, {\"max\": max_stars}\n        )[0]\n        if stars is None:\n            return None\n        return stars\n\n    @staticmethod\n    def get_stage_names(map_names: core.MapNames, chapter_id: int) -> list[str] | None:\n        stage_names = map_names.stage_names.get(chapter_id)\n        if stage_names is None:\n            return None\n        new_stage_names: list[str] = []\n        for stage in stage_names:\n            if stage == \"＠\":\n                continue\n            new_stage_names.append(stage)\n        return new_stage_names\n\n    @staticmethod\n    def ask_stages(map_names: core.MapNames, chapter_id: int) -> list[int] | None:\n        stage_names = EventChapters.get_stage_names(map_names, chapter_id)\n        if stage_names is None:\n            return None\n\n        dialog_creator.ListOutput(\n            stage_names, ints=[], dialog=\"select_stage\", localize_elements=False\n        ).display_locale()\n\n        choices = dialog_creator.RangeInput(len(stage_names), 1).get_input_locale(\n            \"stages_select\", {}\n        )\n        if choices is None:\n            return None\n        return [c - 1 for c in choices]\n\n    @staticmethod\n    def ask_stages_stage_names(stage_names: list[str]) -> list[int] | None:\n        val = EventChapters.ask_stages_stage_names_one(stage_names)\n        if val is None:\n            return None\n        return list(range(val + 1))\n\n    @staticmethod\n    def ask_stages_stage_names_one(stage_names: list[str]) -> int | None:\n        new_stage_names: list[str] = []\n        for stage in stage_names:\n            if stage == \"＠\":\n                continue\n            new_stage_names.append(stage)\n        stage_names = new_stage_names\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            stage_names, dialog=\"select_stage_progress\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return None\n        return choice - 1\n\n    @staticmethod\n    def ask_clear_amount() -> int | None:\n        val = dialog_creator.IntInput(\n            max=core.core_data.max_value_manager.get(\"stage_clear_count\"), bit_count=16\n        ).get_input_locale(\"clear_amount_enter\", {})[0]\n\n        return val\n\n    @staticmethod\n    def edit_sol_chapters(save_file: core.SaveFile):\n        EventChapters.edit_chapters(save_file, 0, \"N\", 0)\n\n    @staticmethod\n    def edit_event_chapters(save_file: core.SaveFile):\n        EventChapters.edit_chapters(save_file, 1, \"S\", 1000)\n\n    @staticmethod\n    def edit_collab_chapters(save_file: core.SaveFile):\n        EventChapters.edit_chapters(save_file, 2, \"C\", 2000)\n\n    @staticmethod\n    def select_map_names(names_dict: dict[int, str | None]) -> list[int] | None:\n        map_ids: list[int] = []\n        names_list: list[str] = []\n        names_dict = dict(sorted(names_dict.items()))\n        ids = list(names_dict.keys())\n        for id, map_name in names_dict.items():\n            if map_name is None:\n                map_name = core.core_data.local_manager.get_key(\n                    \"unknown_map_name\", id=id\n                )\n            else:\n                map_name = core.core_data.local_manager.get_key(\n                    \"map_name\", name=map_name, id=id, escape=False\n                )\n            names_list.append(map_name)\n\n        while True:\n            dialog_creator.ListOutput(\n                names_list, [], \"select_map\", localize_elements=False\n            ).display_locale()\n            if names_list:\n                example_name = names_list[0]\n            else:\n                example_name = \"\"\n            usr_input = (\n                color.ColoredInput()\n                .localize(\"select_map_dialog\", example=example_name, escape=False)\n                .lower()\n                .strip()\n            )\n            if usr_input == \"q\":\n                return None\n            usr_ids = dialog_creator.RangeInput(max=len(names_list), min=1).parse(\n                usr_input\n            )\n            if not usr_ids:\n                found_names: list[tuple[int, str]] = []\n                for i, name in enumerate(names_list):\n                    if usr_input.replace(\" \", \"_\") in name.lower().strip().replace(\n                        \" \", \"_\"\n                    ):\n                        true_id = ids[i]\n                        found_names.append((i, name))\n\n                if len(found_names) == 0:\n                    color.ColoredText.localize(\"no_map_found\", name=usr_input)\n                elif len(found_names) == 1:\n                    id = found_names[0][0]\n                    true_id = ids[id]\n                    if true_id not in map_ids:\n                        map_ids.append(true_id)\n                else:\n                    selected_ids, _ = dialog_creator.ChoiceInput.from_reduced(\n                        [name for _, name in found_names],\n                        dialog=\"select_map_from_names\",\n                    ).multiple_choice(False)\n                    if selected_ids is None:\n                        continue\n                    for i in selected_ids:\n                        id = found_names[i][0]\n                        true_id = ids[id]\n                        if true_id not in map_ids:\n                            map_ids.append(true_id)\n            else:\n                for id in usr_ids:\n                    id -= 1\n                    true_id = ids[id]\n                    if true_id not in map_ids:\n                        map_ids.append(true_id)\n\n            color.ColoredText.localize(\"current_maps\", maps=map_ids)\n\n            for id in map_ids:\n                name = names_dict[id]\n                EventChapters.print_current_chapter(name, id)\n\n            option = dialog_creator.ChoiceInput.from_reduced(\n                [\"keep_selecting\", \"remove_selection\", \"finish_selection\"],\n                dialog=\"map_selection_q\",\n            ).single_choice()\n            if option is None:\n                return None\n\n            option -= 1\n            if option == 0:\n                continue\n            if option == 1:\n                map_ids.clear()\n            else:\n                break\n        return map_ids\n\n    @staticmethod\n    def print_current_chapter(name: str | None, id: int):\n        if name is None:\n            name = core.core_data.local_manager.get_key(\"unknown_map_name\", id=id)\n        color.ColoredText.localize(\n            \"current_sol_chapter\", escape=False, name=name, id=id\n        )\n\n    @staticmethod\n    def print_current_stage(name: str | None, index: int):\n        if name is None:\n            name = core.core_data.local_manager.get_key(\n                \"unknown_stage_name\", index=index\n            )\n        color.ColoredText.localize(\"current_stage_map\", name=name, index=index)\n\n    @staticmethod\n    def edit_chapters(\n        save_file: core.SaveFile, type: int, letter_code: str, base_index: int\n    ):\n        edits.map.edit_chapters(\n            save_file,\n            save_file.event_stages,\n            letter_code,\n            type=type,\n            base_index=base_index,\n        )\n\n    def unclear_rest(\n        self,\n        stages: list[int],\n        stars: int,\n        id: int,\n        type: int,\n    ):\n        if not stages:\n            return\n        for star in range(stars, self.get_total_stars(type, id)):\n            for stage in range(max(stages), self.get_total_stages(type, id, star)):\n                self.chapters[type].chapters[id].chapters[star].stages[\n                    stage\n                ].clear_amount = 0\n                self.chapters[type].chapters[id].chapters[star].clear_progress = 0\n"
  },
  {
    "path": "src/bcsfe/core/game/map/ex_stage.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\n\nclass Stage:\n    def __init__(self, clear_amount: int):\n        self.clear_amount = clear_amount\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read(stream: core.Data) -> Stage:\n        clear_amount = stream.read_int()\n        return Stage(clear_amount)\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.clear_amount)\n\n    def serialize(self) -> int:\n        return self.clear_amount\n\n    @staticmethod\n    def deserialize(data: int) -> Stage:\n        return Stage(data)\n\n    def __repr__(self) -> str:\n        return f\"Stage(clear_amount={self.clear_amount!r})\"\n\n    def __str__(self) -> str:\n        return f\"Stage(clear_amount={self.clear_amount!r})\"\n\n\nclass Chapter:\n    def __init__(self, stages: list[Stage]):\n        self.stages = stages\n\n    @staticmethod\n    def init() -> Chapter:\n        return Chapter([Stage.init() for _ in range(12)])\n\n    @staticmethod\n    def read(stream: core.Data) -> Chapter:\n        total = 12\n        stages: list[Stage] = []\n        for _ in range(total):\n            stages.append(Stage.read(stream))\n        return Chapter(stages)\n\n    def write(self, stream: core.Data):\n        for stage in self.stages:\n            stage.write(stream)\n\n    def serialize(self) -> list[int]:\n        return [stage.serialize() for stage in self.stages]\n\n    @staticmethod\n    def deserialize(data: list[int]) -> Chapter:\n        return Chapter([Stage.deserialize(stage) for stage in data])\n\n    def __repr__(self) -> str:\n        return f\"Chapter(stages={self.stages!r})\"\n\n    def __str__(self) -> str:\n        return f\"Chapter(stages={self.stages!r})\"\n\n\nclass ExChapters:\n    def __init__(self, chapters: list[Chapter]):\n        self.chapters = chapters\n\n    @staticmethod\n    def init() -> ExChapters:\n        return ExChapters([])\n\n    @staticmethod\n    def read(stream: core.Data) -> ExChapters:\n        total = stream.read_int()\n        chapters: list[Chapter] = []\n        for _ in range(total):\n            chapters.append(Chapter.read(stream))\n\n        return ExChapters(chapters)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.chapters))\n        for chapter in self.chapters:\n            chapter.write(stream)\n\n    def serialize(self) -> list[list[int]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[list[int]]) -> ExChapters:\n        return ExChapters([Chapter.deserialize(chapter) for chapter in data])\n\n    def __repr__(self) -> str:\n        return f\"Chapters(chapters={self.chapters!r})\"\n\n    def __str__(self) -> str:\n        return f\"Chapters(chapters={self.chapters!r})\"\n"
  },
  {
    "path": "src/bcsfe/core/game/map/gauntlets.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import edits\n\n\nclass Stage:\n    def __init__(self, clear_times: int):\n        self.clear_times = clear_times\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read(data: core.Data) -> Stage:\n        clear_times = data.read_short()\n        return Stage(clear_times)\n\n    def write(self, data: core.Data):\n        data.write_short(self.clear_times)\n\n    def serialize(self) -> int:\n        return self.clear_times\n\n    @staticmethod\n    def deserialize(data: int) -> Stage:\n        return Stage(\n            data,\n        )\n\n    def __repr__(self):\n        return f\"Stage({self.clear_times})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False):\n        if ensure_cleared_only:\n            self.clear_times = self.clear_times or clear_amount\n        else:\n            self.clear_times = clear_amount\n\n    def unclear_stage(self):\n        self.clear_times = 0\n\n\nclass Chapter:\n    def __init__(self, selected_stage: int, total_stages: int = 0):\n        self.selected_stage = selected_stage\n        self.clear_progress = 0\n        self.stages: list[Stage] = [Stage.init() for _ in range(total_stages)]\n        self.chapter_unlock_state = 0\n        self.total_stages = 0\n\n    def clear_stage(\n        self,\n        index: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        if overwrite_clear_progress:\n            self.clear_progress = index + 1\n        else:\n            self.clear_progress = max(self.clear_progress, index + 1)\n        self.stages[index].clear_stage(clear_amount, ensure_cleared_only)\n        self.chapter_unlock_state = 3\n        if index == self.total_stages - 1:\n            return True\n        return False\n\n    def unclear_stage(self, index: int):\n        self.clear_progress = min(self.clear_progress, index)\n        self.stages[index].unclear_stage()\n        return True\n\n    @staticmethod\n    def init(total_stages: int) -> Chapter:\n        return Chapter(0, total_stages)\n\n    @staticmethod\n    def read_selected_stage(data: core.Data) -> Chapter:\n        selected_stage = data.read_byte()\n        return Chapter(selected_stage)\n\n    def write_selected_stage(self, data: core.Data):\n        data.write_byte(self.selected_stage)\n\n    def read_clear_progress(self, data: core.Data):\n        self.clear_progress = data.read_byte()\n\n    def write_clear_progress(self, data: core.Data):\n        data.write_byte(self.clear_progress)\n\n    def read_stages(self, data: core.Data, total_stages: int):\n        self.stages = [Stage.read(data) for _ in range(total_stages)]\n\n    def write_stages(self, data: core.Data):\n        for stage in self.stages:\n            stage.write(data)\n\n    def read_chapter_unlock_state(self, data: core.Data):\n        self.chapter_unlock_state = data.read_byte()\n\n    def write_chapter_unlock_state(self, data: core.Data):\n        data.write_byte(self.chapter_unlock_state)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"selected_stage\": self.selected_stage,\n            \"clear_progress\": self.clear_progress,\n            \"stages\": [stage.serialize() for stage in self.stages],\n            \"chapter_unlock_state\": self.chapter_unlock_state,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Chapter:\n        chapter = Chapter(data.get(\"selected_stage\", 0))\n        chapter.clear_progress = data.get(\"clear_progress\", 0)\n        chapter.stages = [Stage.deserialize(stage) for stage in data.get(\"stages\", [])]\n        chapter.chapter_unlock_state = data.get(\"chapter_unlock_state\", 0)\n        return chapter\n\n    def __repr__(self):\n        return f\"Chapter({self.selected_stage}, {self.clear_progress}, {self.stages}, {self.chapter_unlock_state})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ChaptersStars:\n    def __init__(self, chapters: list[Chapter]):\n        self.chapters = chapters\n\n    def clear_stage(\n        self,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[star].clear_stage(\n            stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished:\n            if star + 1 < len(self.chapters):\n                self.chapters[star + 1].chapter_unlock_state = 1\n        return finished\n\n    def unclear_stage(self, star: int, stage: int):\n        finished = self.chapters[star].unclear_stage(stage)\n        if finished and star + 1 < len(self.chapters):\n            for chapter in self.chapters[star + 1 :]:\n                chapter.chapter_unlock_state = 0\n        return finished\n\n    @staticmethod\n    def init(total_stages: int, total_stars: int) -> ChaptersStars:\n        chapters = [Chapter.init(total_stages) for _ in range(total_stars)]\n        return ChaptersStars(chapters)\n\n    @staticmethod\n    def read_selected_stage(data: core.Data, total_stars: int) -> ChaptersStars:\n        chapters = [Chapter.read_selected_stage(data) for _ in range(total_stars)]\n        return ChaptersStars(chapters)\n\n    def write_selected_stage(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data)\n\n    def read_clear_progress(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.read_clear_progress(data)\n\n    def write_clear_progress(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data)\n\n    def read_stages(self, data: core.Data, total_stages: int):\n        for _ in range(total_stages):\n            for chapter in self.chapters:\n                chapter.stages.append(Stage.read(data))\n\n    def write_stages(self, data: core.Data):\n        for i in range(len(self.chapters[0].stages)):\n            for chapter in self.chapters:\n                chapter.stages[i].write(data)\n\n    def read_chapter_unlock_state(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.read_chapter_unlock_state(data)\n\n    def write_chapter_unlock_state(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> ChaptersStars:\n        chapters = [Chapter.deserialize(chapter) for chapter in data]\n        return ChaptersStars(chapters)\n\n    def __repr__(self):\n        return f\"ChaptersStars({self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass GauntletChapters:\n    def __init__(self, chapters: list[ChaptersStars], unknown: list[int]):\n        self.chapters = chapters\n        self.unknown = unknown\n\n    def clear_stage(\n        self,\n        map: int,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[map].clear_stage(\n            star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished and map + 1 < len(self.chapters):\n            self.chapters[map + 1].chapters[0].chapter_unlock_state = 1\n\n        return finished\n\n    def unclear_stage(self, map: int, star: int, stage: int) -> bool:\n        finished = self.chapters[map].unclear_stage(star, stage)\n        if finished and map + 1 < len(self.chapters) and star == 0:\n            for chapter in self.chapters[map + 1].chapters:\n                chapter.chapter_unlock_state = 0\n\n        return finished\n\n    @staticmethod\n    def init() -> GauntletChapters:\n        return GauntletChapters([], [])\n\n    @staticmethod\n    def read(data: core.Data) -> GauntletChapters:\n        total_chapters = data.read_short()\n        total_stages = data.read_byte()\n        total_stars = data.read_byte()\n\n        chapters = [\n            ChaptersStars.read_selected_stage(data, total_stars)\n            for _ in range(total_chapters)\n        ]\n\n        for chapter in chapters:\n            chapter.read_clear_progress(data)\n\n        for chapter in chapters:\n            chapter.read_stages(data, total_stages)\n\n        for chapter in chapters:\n            chapter.read_chapter_unlock_state(data)\n\n        unknown = [data.read_byte() for _ in range(total_chapters)]\n\n        return GauntletChapters(chapters, unknown)\n\n    def write(self, data: core.Data):\n        data.write_short(len(self.chapters))\n        try:\n            data.write_byte(len(self.chapters[0].chapters[0].stages))\n        except IndexError:\n            data.write_byte(0)\n        try:\n            data.write_byte(len(self.chapters[0].chapters))\n        except IndexError:\n            data.write_byte(0)\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data)\n\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data)\n\n        for chapter in self.chapters:\n            chapter.write_stages(data)\n\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data)\n\n        for unknown in self.unknown:\n            data.write_byte(unknown)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": [chapter.serialize() for chapter in self.chapters],\n            \"unknown\": self.unknown,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> GauntletChapters:\n        chapters = [\n            ChaptersStars.deserialize(chapter) for chapter in data.get(\"chapters\", [])\n        ]\n        return GauntletChapters(chapters, data.get(\"unknown\", []))\n\n    def __repr__(self):\n        return f\"Chapters({self.chapters}, {self.unknown})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def get_total_stars(self, map: int) -> int:\n        try:\n            return len(self.chapters[map].chapters)\n        except IndexError:\n            return 0\n\n    def get_total_stages(self, map: int, star: int) -> int:\n        try:\n            return len(self.chapters[map].chapters[star].stages)\n        except IndexError:\n            return 0\n\n    @staticmethod\n    def edit_gauntlets(save_file: core.SaveFile):\n        gauntlets = save_file.gauntlets\n        gauntlets.edit_chapters(save_file, \"A\", 24000)\n\n    @staticmethod\n    def edit_collab_gauntlets(save_file: core.SaveFile):\n        gauntlets = save_file.collab_gauntlets\n        gauntlets.edit_chapters(save_file, \"CA\", 27000)\n\n    @staticmethod\n    def edit_behemoth_culling(save_file: core.SaveFile):\n        gauntlets = save_file.behemoth_culling\n        gauntlets.edit_chapters(save_file, \"Q\", 31000)\n\n    @staticmethod\n    def edit_enigma_stages(save_file: core.SaveFile):\n        save_file.enigma_clears.edit_chapters(save_file, \"H\", 25000)\n\n    def edit_chapters(\n        self, save_file: core.SaveFile, letter_code: str, base_index: int\n    ):\n        edits.map.edit_chapters(save_file, self, letter_code, base_index=base_index)\n\n    def unclear_rest(self, stages: list[int], stars: int, id: int):\n        if not stages:\n            return\n        for star in range(stars, self.get_total_stars(id)):\n            for stage in range(max(stages), self.get_total_stages(id, star)):\n                self.chapters[id].chapters[star].stages[stage].clear_times = 0\n                self.chapters[id].chapters[star].clear_progress = 0\n\n    def set_total_stages(self, map: int, total_stages: int):\n        for chapter in self.chapters[map].chapters:\n            chapter.total_stages = total_stages\n"
  },
  {
    "path": "src/bcsfe/core/game/map/item_reward_stage.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass Stage:\n    def __init__(self, claimed: bool):\n        self.claimed = claimed\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(False)\n\n    @staticmethod\n    def read(stream: core.Data) -> Stage:\n        return Stage(stream.read_bool())\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.claimed)\n\n    def serialize(self) -> bool:\n        return self.claimed\n\n    @staticmethod\n    def deserialize(data: bool) -> Stage:\n        return Stage(data)\n\n    def __repr__(self) -> str:\n        return f\"Stage(claimed={self.claimed})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass SubChapter:\n    def __init__(self, stages: list[Stage]):\n        self.stages = stages\n\n    @staticmethod\n    def init(total_stages: int) -> SubChapter:\n        stages = [Stage.init() for _ in range(total_stages)]\n        return SubChapter(stages)\n\n    @staticmethod\n    def read(stream: core.Data, total_stages: int) -> SubChapter:\n        stages: list[Stage] = []\n        for _ in range(total_stages):\n            stages.append(Stage.read(stream))\n        return SubChapter(stages)\n\n    def write(self, stream: core.Data):\n        for stage in self.stages:\n            stage.write(stream)\n\n    def serialize(self) -> list[bool]:\n        return [stage.serialize() for stage in self.stages]\n\n    @staticmethod\n    def deserialize(data: list[bool]) -> SubChapter:\n        return SubChapter([Stage.deserialize(stage) for stage in data])\n\n    def __repr__(self) -> str:\n        return f\"SubChapter(stages={self.stages})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass SubChapterStars:\n    def __init__(self, sub_chapters: list[SubChapter]):\n        self.sub_chapters = sub_chapters\n\n    @staticmethod\n    def init(total_stages: int, total_stars: int) -> SubChapterStars:\n        sub_chapters = [\n            SubChapter.init(total_stages) for _ in range(total_stars)\n        ]\n        return SubChapterStars(sub_chapters)\n\n    @staticmethod\n    def read(\n        stream: core.Data, total_stages: int, total_stars: int\n    ) -> SubChapterStars:\n        sub_chapters: list[SubChapter] = []\n        for _ in range(total_stars):\n            sub_chapters.append(SubChapter.read(stream, total_stages))\n        return SubChapterStars(sub_chapters)\n\n    def write(self, stream: core.Data):\n        for sub_chapter in self.sub_chapters:\n            sub_chapter.write(stream)\n\n    def serialize(self) -> list[list[bool]]:\n        return [sub_chapter.serialize() for sub_chapter in self.sub_chapters]\n\n    @staticmethod\n    def deserialize(data: list[list[bool]]) -> SubChapterStars:\n        return SubChapterStars(\n            [SubChapter.deserialize(sub_chapter) for sub_chapter in data]\n        )\n\n    def __repr__(self) -> str:\n        return f\"SubChapterStars(sub_chapters={self.sub_chapters})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass ItemObtain:\n    def __init__(self, flag: bool):\n        self.flag = flag\n\n    @staticmethod\n    def init() -> ItemObtain:\n        return ItemObtain(False)\n\n    @staticmethod\n    def read(stream: core.Data) -> ItemObtain:\n        return ItemObtain(stream.read_bool())\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.flag)\n\n    def serialize(self) -> bool:\n        return self.flag\n\n    @staticmethod\n    def deserialize(data: bool) -> ItemObtain:\n        return ItemObtain(data)\n\n    def __repr__(self) -> str:\n        return f\"ItemObtain(flag={self.flag})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass ItemObtainSet:\n    def __init__(self, item_obtains: dict[int, ItemObtain]):\n        self.item_obtains = item_obtains\n\n    @staticmethod\n    def init() -> ItemObtainSet:\n        return ItemObtainSet({})\n\n    @staticmethod\n    def read(stream: core.Data) -> ItemObtainSet:\n        item_obtains: dict[int, ItemObtain] = {}\n        for _ in range(stream.read_int()):\n            key = stream.read_int()\n            item_obtains[key] = ItemObtain.read(stream)\n        return ItemObtainSet(item_obtains)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.item_obtains))\n        for item_id, item_obtain in self.item_obtains.items():\n            stream.write_int(item_id)\n            item_obtain.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"item_obtains\": {\n                item_id: item_obtain.serialize()\n                for item_id, item_obtain in self.item_obtains.items()\n            }\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> ItemObtainSet:\n        return ItemObtainSet(\n            {\n                int(item_id): ItemObtain.deserialize(item_obtain)\n                for item_id, item_obtain in data.get(\"item_obtains\", {}).items()\n            }\n        )\n\n    def __repr__(self) -> str:\n        return f\"ItemObtainSet(item_obtains={self.item_obtains})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass ItemObtainSets:\n    def __init__(self, item_obtain_sets: dict[int, ItemObtainSet]):\n        self.item_obtain_sets = item_obtain_sets\n\n    @staticmethod\n    def init() -> ItemObtainSets:\n        return ItemObtainSets({})\n\n    @staticmethod\n    def read(stream: core.Data) -> ItemObtainSets:\n        item_obtain_sets: dict[int, ItemObtainSet] = {}\n        for _ in range(stream.read_int()):\n            key = stream.read_int()\n            item_obtain_sets[key] = ItemObtainSet.read(stream)\n        return ItemObtainSets(item_obtain_sets)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.item_obtain_sets))\n        for item_id, item_obtain_set in self.item_obtain_sets.items():\n            stream.write_int(item_id)\n            item_obtain_set.write(stream)\n\n    def serialize(self) -> dict[int, Any]:\n        return {\n            item_id: item_obtain_set.serialize()\n            for item_id, item_obtain_set in self.item_obtain_sets.items()\n        }\n\n    @staticmethod\n    def deserialize(data: dict[int, Any]) -> ItemObtainSets:\n        return ItemObtainSets(\n            {\n                int(item_id): ItemObtainSet.deserialize(item_obtain_set)\n                for item_id, item_obtain_set in data.items()\n            }\n        )\n\n    def __repr__(self) -> str:\n        return f\"ItemObtainSets(item_obtain_sets={self.item_obtain_sets})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass UnobtainedItem:\n    def __init__(self, unobtained: bool):\n        self.unobtained = unobtained\n\n    @staticmethod\n    def init() -> UnobtainedItem:\n        return UnobtainedItem(False)\n\n    @staticmethod\n    def read(stream: core.Data) -> UnobtainedItem:\n        return UnobtainedItem(stream.read_bool())\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.unobtained)\n\n    def serialize(self) -> bool:\n        return self.unobtained\n\n    @staticmethod\n    def deserialize(data: bool) -> UnobtainedItem:\n        return UnobtainedItem(data)\n\n    def __repr__(self) -> str:\n        return f\"UnobtainedItem(unobtained={self.unobtained})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass UnobtainedItems:\n    def __init__(self, unobtained_items: dict[int, UnobtainedItem]):\n        self.unobtained_items = unobtained_items\n\n    @staticmethod\n    def init() -> UnobtainedItems:\n        return UnobtainedItems({})\n\n    @staticmethod\n    def read(stream: core.Data) -> UnobtainedItems:\n        unobtained_items: dict[int, UnobtainedItem] = {}\n        for _ in range(stream.read_int()):\n            key = stream.read_int()\n            unobtained_items[key] = UnobtainedItem.read(stream)\n        return UnobtainedItems(unobtained_items)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.unobtained_items))\n        for item_id, unobtained_item in self.unobtained_items.items():\n            stream.write_int(item_id)\n            unobtained_item.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"unobtained_items\": {\n                item_id: unobtained_item.serialize()\n                for item_id, unobtained_item in self.unobtained_items.items()\n            }\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> UnobtainedItems:\n        return UnobtainedItems(\n            {\n                int(item_id): UnobtainedItem.deserialize(unobtained_item)\n                for item_id, unobtained_item in data.get(\n                    \"unobtained_items\", {}\n                ).items()\n            }\n        )\n\n    def __repr__(self) -> str:\n        return f\"UnobtainedItems(unobtained_items={self.unobtained_items})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass ItemRewardChapters:\n    def __init__(self, sub_chapters: list[SubChapterStars]):\n        self.sub_chapters = sub_chapters\n        self.item_obtains = ItemObtainSets.init()\n        self.unobtained_items = UnobtainedItems.init()\n\n    @staticmethod\n    def init(gv: core.GameVersion) -> ItemRewardChapters:\n        if gv < 20:\n            return ItemRewardChapters([])\n        if gv <= 33:\n            total_subchapters = 50\n            total_stages = 12\n            total_stars = 3\n        elif gv <= 34:\n            total_subchapters = 0\n            total_stages = 12\n            total_stars = 3\n        else:\n            total_subchapters = 0\n            total_stages = 0\n            total_stars = 0\n        return ItemRewardChapters(\n            [\n                SubChapterStars.init(total_stages, total_stars)\n                for _ in range(total_subchapters)\n            ]\n        )\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> ItemRewardChapters:\n        if gv < 20:\n            return ItemRewardChapters([])\n        if gv <= 33:\n            total_subchapters = 50\n            total_stages = 12\n            total_stars = 3\n        elif gv <= 34:\n            total_subchapters = stream.read_int()\n            total_stages = 12\n            total_stars = 3\n        else:\n            total_subchapters = stream.read_int()\n            total_stages = stream.read_int()\n            total_stars = stream.read_int()\n        sub_chapters: list[SubChapterStars] = []\n        for _ in range(total_subchapters):\n            sub_chapters.append(\n                SubChapterStars.read(stream, total_stages, total_stars)\n            )\n        return ItemRewardChapters(sub_chapters)\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        if gv < 20:\n            return\n        if gv <= 33:\n            pass\n        elif gv <= 34:\n            stream.write_int(len(self.sub_chapters))\n        else:\n            stream.write_int(len(self.sub_chapters))\n            try:\n                stream.write_int(\n                    len(self.sub_chapters[0].sub_chapters[0].stages)\n                )\n            except IndexError:\n                stream.write_int(0)\n            try:\n                stream.write_int(len(self.sub_chapters[0].sub_chapters))\n            except IndexError:\n                stream.write_int(0)\n        for sub_chapter in self.sub_chapters:\n            sub_chapter.write(stream)\n\n    def read_item_obtains(self, stream: core.Data):\n        self.item_obtains = ItemObtainSets.read(stream)\n        self.unobtained_items = UnobtainedItems.read(stream)\n\n    def write_item_obtains(self, stream: core.Data):\n        self.item_obtains.write(stream)\n        self.unobtained_items.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"sub_chapters\": [\n                sub_chapter.serialize() for sub_chapter in self.sub_chapters\n            ],\n            \"item_obtains\": self.item_obtains.serialize(),\n            \"unobtained_items\": self.unobtained_items.serialize(),\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> ItemRewardChapters:\n        chapters = ItemRewardChapters(\n            [\n                SubChapterStars.deserialize(sub_chapter)\n                for sub_chapter in data.get(\"sub_chapters\", [])\n            ]\n        )\n        chapters.item_obtains = ItemObtainSets.deserialize(\n            data.get(\"item_obtains\", {})\n        )\n        chapters.unobtained_items = UnobtainedItems.deserialize(\n            data.get(\"unobtained_items\", {})\n        )\n        return chapters\n\n    def __repr__(self) -> str:\n        return f\"Chapters(sub_chapters={self.sub_chapters}, item_obtains={self.item_obtains}, unobtained_items={self.unobtained_items})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n"
  },
  {
    "path": "src/bcsfe/core/game/map/legend_quest.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import edits\n\n\nclass Stage:\n    def __init__(self, clear_times: int):\n        self.clear_times = clear_times\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read(data: core.Data) -> Stage:\n        clear_times = data.read_short()\n        return Stage(clear_times)\n\n    def write(self, data: core.Data):\n        data.write_short(self.clear_times)\n\n    def read_tries(self, data: core.Data):\n        self.tries = data.read_short()\n\n    def write_tries(self, data: core.Data):\n        data.write_short(self.tries)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"clear_times\": self.clear_times,\n            \"tries\": self.tries,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Stage:\n        stage = Stage(\n            data.get(\"clear_times\", 0),\n        )\n        stage.tries = data.get(\"tries\", 0)\n        return stage\n\n    def __repr__(self):\n        return f\"Stage({self.clear_times}, {self.tries})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False):\n        if ensure_cleared_only:\n            self.clear_times = self.clear_times or clear_amount\n            self.tries = self.tries or clear_amount\n        else:\n            self.clear_times = clear_amount\n            self.tries = clear_amount\n\n    def unclear_stage(self):\n        self.clear_times = 0\n        self.tries = 0\n\n\nclass Chapter:\n    def __init__(self, selected_stage: int, total_stages: int = 0):\n        self.selected_stage = selected_stage\n        self.clear_progress = 0\n        self.stages: list[Stage] = [Stage.init() for _ in range(total_stages)]\n        self.chapter_unlock_state = 0\n\n        self.total_stages = 0\n\n    def clear_stage(\n        self,\n        index: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        if overwrite_clear_progress:\n            self.clear_progress = index + 1\n        else:\n            self.clear_progress = max(self.clear_progress, index + 1)\n        self.stages[index].clear_stage(clear_amount, ensure_cleared_only)\n        self.chapter_unlock_state = 3\n        if index == self.total_stages - 1:\n            return True\n        return False\n\n    def unclear_stage(self, index: int) -> bool:\n        self.clear_progress = min(self.clear_progress, index)\n        self.stages[index].unclear_stage()\n        return True\n\n    @staticmethod\n    def init(total_stages: int) -> Chapter:\n        return Chapter(0, total_stages)\n\n    @staticmethod\n    def read_selected_stage(data: core.Data) -> Chapter:\n        selected_stage = data.read_byte()\n        return Chapter(selected_stage)\n\n    def write_selected_stage(self, data: core.Data):\n        data.write_byte(self.selected_stage)\n\n    def read_clear_progress(self, data: core.Data):\n        self.clear_progress = data.read_byte()\n\n    def write_clear_progress(self, data: core.Data):\n        data.write_byte(self.clear_progress)\n\n    def read_stages(self, data: core.Data, total_stages: int):\n        self.stages = [Stage.read(data) for _ in range(total_stages)]\n        for stage in self.stages:\n            stage.read_tries(data)\n\n    def write_stages(self, data: core.Data):\n        for stage in self.stages:\n            stage.write(data)\n\n        for stage in self.stages:\n            stage.write_tries(data)\n\n    def read_chapter_unlock_state(self, data: core.Data):\n        self.chapter_unlock_state = data.read_byte()\n\n    def write_chapter_unlock_state(self, data: core.Data):\n        data.write_byte(self.chapter_unlock_state)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"selected_stage\": self.selected_stage,\n            \"clear_progress\": self.clear_progress,\n            \"stages\": [stage.serialize() for stage in self.stages],\n            \"chapter_unlock_state\": self.chapter_unlock_state,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Chapter:\n        chapter = Chapter(\n            data.get(\"selected_stage\", 0),\n        )\n        chapter.clear_progress = data.get(\"clear_progress\", 0)\n        chapter.stages = [Stage.deserialize(stage) for stage in data.get(\"stages\", [])]\n        chapter.chapter_unlock_state = data.get(\"chapter_unlock_state\", 0)\n        return chapter\n\n    def __repr__(self):\n        return f\"Chapter({self.selected_stage}, {self.clear_progress}, {self.stages}, {self.chapter_unlock_state})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ChaptersStars:\n    def __init__(self, chapters: list[Chapter]):\n        self.chapters = chapters\n\n    def clear_stage(\n        self,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[star].clear_stage(\n            stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished:\n            if star + 1 < len(self.chapters):\n                self.chapters[star + 1].chapter_unlock_state = 1\n        return finished\n\n    def unclear_stage(self, star: int, stage: int) -> bool:\n        finished = self.chapters[star].unclear_stage(stage)\n        if finished and star + 1 < len(self.chapters):\n            for chapter in self.chapters[star + 1 :]:\n                chapter.chapter_unlock_state = 0\n        return finished\n\n    @staticmethod\n    def init(total_stages: int, total_stars: int) -> ChaptersStars:\n        chapters = [Chapter.init(total_stages) for _ in range(total_stars)]\n        return ChaptersStars(chapters)\n\n    @staticmethod\n    def read_selected_stage(data: core.Data, total_stars: int) -> ChaptersStars:\n        chapters = [Chapter.read_selected_stage(data) for _ in range(total_stars)]\n        return ChaptersStars(chapters)\n\n    def write_selected_stage(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data)\n\n    def read_clear_progress(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.read_clear_progress(data)\n\n    def write_clear_progress(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data)\n\n    def read_stages(self, data: core.Data, total_stages: int):\n        for _ in range(total_stages):\n            for chapter in self.chapters:\n                chapter.stages.append(Stage.read(data))\n\n        for i in range(total_stages):\n            for chapter in self.chapters:\n                chapter.stages[i].read_tries(data)\n\n    def write_stages(self, data: core.Data):\n        for i in range(len(self.chapters[0].stages)):\n            for chapter in self.chapters:\n                chapter.stages[i].write(data)\n\n        for i in range(len(self.chapters[0].stages)):\n            for chapter in self.chapters:\n                chapter.stages[i].write_tries(data)\n\n    def read_chapter_unlock_state(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.read_chapter_unlock_state(data)\n\n    def write_chapter_unlock_state(self, data: core.Data):\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> ChaptersStars:\n        chapters = [Chapter.deserialize(chapter) for chapter in data]\n        return ChaptersStars(chapters)\n\n    def __repr__(self):\n        return f\"ChaptersStars({self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass LegendQuestChapters:\n    def __init__(\n        self, chapters: list[ChaptersStars], unknown: list[int], ids: list[int]\n    ):\n        self.chapters = chapters\n        self.unknown = unknown\n        self.ids = ids\n\n    def clear_stage(\n        self,\n        map: int,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[map].clear_stage(\n            star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished and map + 1 < len(self.chapters):\n            self.chapters[map + 1].chapters[0].chapter_unlock_state = 1\n\n        return finished\n\n    def unclear_stage(self, map: int, star: int, stage: int) -> bool:\n        finished = self.chapters[map].unclear_stage(star, stage)\n        if finished and map + 1 < len(self.chapters) and star == 0:\n            for chapter in self.chapters[map + 1].chapters:\n                chapter.chapter_unlock_state = 0\n\n        return finished\n\n    @staticmethod\n    def init() -> LegendQuestChapters:\n        return LegendQuestChapters([], [], [])\n\n    @staticmethod\n    def read(data: core.Data) -> LegendQuestChapters:\n        total_chapters = data.read_byte()\n        total_stages = data.read_byte()\n        total_stars = data.read_byte()\n\n        chapters = [\n            ChaptersStars.read_selected_stage(data, total_stars)\n            for _ in range(total_chapters)\n        ]\n\n        for chapter in chapters:\n            chapter.read_clear_progress(data)\n\n        for chapter in chapters:\n            chapter.read_stages(data, total_stages)\n\n        for chapter in chapters:\n            chapter.read_chapter_unlock_state(data)\n\n        unknown = [data.read_byte() for _ in range(total_chapters)]\n        ids = [data.read_int() for _ in range(total_stages)]\n\n        return LegendQuestChapters(chapters, unknown, ids)\n\n    def write(self, data: core.Data):\n        data.write_byte(len(self.chapters))\n        try:\n            data.write_byte(len(self.chapters[0].chapters[0].stages))\n        except IndexError:\n            data.write_byte(0)\n        try:\n            data.write_byte(len(self.chapters[0].chapters))\n        except IndexError:\n            data.write_byte(0)\n\n        for chapter in self.chapters:\n            chapter.write_selected_stage(data)\n\n        for chapter in self.chapters:\n            chapter.write_clear_progress(data)\n\n        for chapter in self.chapters:\n            chapter.write_stages(data)\n\n        for chapter in self.chapters:\n            chapter.write_chapter_unlock_state(data)\n\n        for unknown in self.unknown:\n            data.write_byte(unknown)\n\n        for id in self.ids:\n            data.write_int(id)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": [chapter.serialize() for chapter in self.chapters],\n            \"unknown\": self.unknown,\n            \"ids\": self.ids,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> LegendQuestChapters:\n        chapters = [\n            ChaptersStars.deserialize(chapter) for chapter in data.get(\"chapters\", [])\n        ]\n        unknown = data.get(\"unknown\", [])\n        ids = data.get(\"ids\", [])\n        return LegendQuestChapters(chapters, unknown, ids)\n\n    def __repr__(self):\n        return f\"Chapters({self.chapters}, {self.unknown}, {self.ids})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def get_total_stars(self, map: int) -> int:\n        try:\n            return len(self.chapters[map].chapters)\n        except IndexError:\n            return 0\n\n    def get_total_stages(self, map: int, star: int) -> int:\n        try:\n            return len(self.chapters[map].chapters[star].stages)\n        except IndexError:\n            return 0\n\n    @staticmethod\n    def edit_legend_quest(save_file: core.SaveFile):\n        legend_quest = save_file.legend_quest\n        legend_quest.edit_chapters(save_file, \"D\", base_index=16000)\n\n    def edit_chapters(\n        self, save_file: core.SaveFile, letter_code: str, base_index: int\n    ):\n        edits.map.edit_chapters(save_file, self, letter_code, base_index=base_index)\n\n    def unclear_rest(self, stages: list[int], stars: int, id: int):\n        if not stages:\n            return\n        for star in range(stars, self.get_total_stars(id)):\n            for stage in range(max(stages), self.get_total_stages(id, star)):\n                self.chapters[id].chapters[star].stages[stage].clear_times = 0\n                self.chapters[id].chapters[star].clear_progress = 0\n\n    def set_total_stages(self, map: int, total_stages: int):\n        for chapter in self.chapters[map].chapters:\n            chapter.total_stages = total_stages\n"
  },
  {
    "path": "src/bcsfe/core/game/map/map_names.py",
    "content": "from __future__ import annotations\n\nfrom bcsfe import core\n\n\nclass MapNames:\n    def __init__(\n        self,\n        save_file: core.SaveFile,\n        code: str,\n        base_index: int,\n        output: bool = True,\n        no_r_prefix: bool = False,\n    ):\n        self.save_file = save_file\n        self.out = output\n        self.code = code\n        self.base_index = base_index\n        self.no_r_prefix = no_r_prefix\n        self.gdg = core.core_data.get_game_data_getter(self.save_file)\n        self.map_names: dict[int, str | None] = {}\n        self.stage_names: dict[int, list[str]] = {}\n        self.get_map_names()\n\n    def get_map_names_in_game(\n        self, base_index: int, total_stages: int\n    ) -> dict[int, str | None] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        map_name_data = gdg.download(\"resLocal\", \"Map_Name.csv\")\n        if map_name_data is None:\n            return None\n\n        csv = core.CSV(\n            map_name_data, core.Delimeter.from_country_code_res(self.save_file.cc)\n        )\n        names: dict[int, str | None] = {}\n        for row in csv:\n            id = row[0].to_int()\n            name = row[1].to_str().strip()\n\n            for i in range(total_stages):\n                index = i + base_index\n                if id == index:\n                    if name:\n                        names[i] = name\n                    else:\n                        names[i] = None\n                    break\n\n        return names\n\n    def get_map_names(self) -> dict[int, str | None] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        r_prefix = \"\" if self.no_r_prefix else \"R\"\n        stage_names = gdg.download(\n            \"resLocal\",\n            f\"StageName_{r_prefix}{self.code}_{core.core_data.get_lang(self.save_file)}.csv\",\n        )\n        if stage_names is None:\n            return None\n        csv = core.CSV(\n            stage_names,\n            core.Delimeter.from_country_code_res(self.save_file.cc),\n        )\n        for i, row in enumerate(csv):\n            stage_names_row = row.to_str_list()\n            if not stage_names_row:\n                continue\n            self.stage_names[i] = stage_names_row\n\n        names = self.get_map_names_in_game(self.base_index, len(self.stage_names))\n        if names is None:\n            return None\n        self.map_names = names\n        return self.map_names\n\n    @staticmethod\n    def get_code_from_id(id: int) -> str | None:\n        base_id = id // 1000\n\n        ids = {\n            0: \"RN\",\n            1: \"RS\",\n            2: \"RC\",\n            4: \"EX\",\n            6: \"RT\",\n            7: \"RV\",\n            11: \"RR\",\n            12: \"RM\",\n            13: \"RNA\",\n            14: \"RB\",\n            16: \"RD\",\n            20: \"Z\",\n            21: \"Z\",\n            22: \"Z\",\n            24: \"RA\",\n            25: \"RH\",\n            27: \"RCA\",\n            30: \"DM\",\n            31: \"RQ\",\n            32: \"L\",\n            34: \"RND\",\n        }\n\n        return ids.get(base_id)\n\n    @staticmethod\n    def from_id(id: int, save_file: core.SaveFile) -> MapNames | None:\n        code = MapNames.get_code_from_id(id)\n        if code is None:\n            return None\n        return MapNames(save_file, code, id, no_r_prefix=True)\n"
  },
  {
    "path": "src/bcsfe/core/game/map/map_option.py",
    "content": "from __future__ import annotations\n\nfrom bcsfe import core\n\n\nclass MapOptionLine:\n    def __init__(\n        self,\n        map_id: int,\n        crown_count: int,\n        crown_mults: list[int],\n        guerrilla_set: int,\n        reset_type: int,\n        one_time_display: bool,\n        display_order: int,\n        interval: int,\n        challenge_flag: bool,\n        difficulty_mask: int,\n        hide_after_clear: bool,\n        name: str,\n    ):\n        self.map_id = map_id\n        self.crown_count = crown_count\n        self.crown_mults = crown_mults\n        self.guerrilla_set = guerrilla_set\n        self.reset_type = reset_type\n        self.one_time_display = one_time_display\n        self.display_order = display_order\n        self.interval = interval\n        self.challenge_flag = challenge_flag\n        self.difficulty_mask = difficulty_mask\n        self.hide_after_clear = hide_after_clear\n        self.name = name\n\n    @staticmethod\n    def from_line(line: core.Row) -> MapOptionLine:\n        return MapOptionLine(\n            line.next_int(),\n            line.next_int(),\n            [line.next_int() for _ in range(4)],\n            line.next_int(),\n            line.next_int(),\n            line.next_bool(),\n            line.next_int(),\n            line.next_int(),\n            line.next_bool(),\n            line.next_int(),\n            line.next_bool(),\n            line.next_str(),\n        )\n\n\nclass MapOption:\n    def __init__(self, maps: dict[int, MapOptionLine]):\n        self.maps = maps\n\n    @staticmethod\n    def from_csv(csv: core.CSV) -> MapOption:\n        data: dict[int, MapOptionLine] = {}\n\n        for line in csv.lines[1:]:  # skip headers\n            item = MapOptionLine.from_line(line)\n            data[item.map_id] = item\n\n        return MapOption(data)\n\n    @staticmethod\n    def from_save(save_file: core.SaveFile) -> MapOption | None:\n        gdg = core.core_data.get_game_data_getter(save_file)\n        data = gdg.download(\"DataLocal\", \"Map_option.csv\")\n        if data is None:\n            return None\n\n        csv = core.CSV(data)\n\n        return MapOption.from_csv(csv)\n\n    def get_map(self, map_id: int) -> MapOptionLine | None:\n        return self.maps.get(map_id)\n"
  },
  {
    "path": "src/bcsfe/core/game/map/map_reset.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\n\nclass MapResetData:\n    def __init__(\n        self,\n        yearly_end_timestamp: float,\n        monthly_end_timestamp: float,\n        weekly_end_timestamp: float,\n        daily_end_timestamp: float,\n    ):\n        self.yearly_end_timestamp = yearly_end_timestamp\n        self.monthly_end_timestamp = monthly_end_timestamp\n        self.weekly_end_timestamp = weekly_end_timestamp\n        self.daily_end_timestamp = daily_end_timestamp\n\n    @staticmethod\n    def init() -> MapResetData:\n        return MapResetData(\n            0.0,\n            0.0,\n            0.0,\n            0.0,\n        )\n\n    @staticmethod\n    def read(stream: core.Data) -> MapResetData:\n        yearly_end_timestamp = stream.read_double()\n        monthly_end_timestamp = stream.read_double()\n        weekly_end_timestamp = stream.read_double()\n        daily_end_timestamp = stream.read_double()\n        return MapResetData(\n            yearly_end_timestamp,\n            monthly_end_timestamp,\n            weekly_end_timestamp,\n            daily_end_timestamp,\n        )\n\n    def write(self, stream: core.Data):\n        stream.write_double(self.yearly_end_timestamp)\n        stream.write_double(self.monthly_end_timestamp)\n        stream.write_double(self.weekly_end_timestamp)\n        stream.write_double(self.daily_end_timestamp)\n\n    def serialize(self) -> dict[str, float]:\n        return {\n            \"yearly_end_timestamp\": self.yearly_end_timestamp,\n            \"monthly_end_timestamp\": self.monthly_end_timestamp,\n            \"weekly_end_timestamp\": self.weekly_end_timestamp,\n            \"daily_end_timestamp\": self.daily_end_timestamp,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, float]) -> MapResetData:\n        return MapResetData(\n            data.get(\"yearly_end_timestamp\", 0.0),\n            data.get(\"monthly_end_timestamp\", 0.0),\n            data.get(\"weekly_end_timestamp\", 0.0),\n            data.get(\"daily_end_timestamp\", 0.0),\n        )\n\n    def __str__(self) -> str:\n        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})\"\n\n    def __repr__(self) -> str:\n        return str(self)\n\n\nclass MapResets:\n    def __init__(self, data: dict[int, list[MapResetData]]):\n        self.data = data\n\n    @staticmethod\n    def init() -> MapResets:\n        return MapResets({})\n\n    @staticmethod\n    def read(stream: core.Data) -> MapResets:\n        data: dict[int, list[MapResetData]] = {}\n        for _ in range(stream.read_int()):\n            key = stream.read_int()\n            value: list[MapResetData] = []\n            for _ in range(stream.read_int()):\n                value.append(MapResetData.read(stream))\n            data[key] = value\n        return MapResets(data)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.data))\n        for key, value in self.data.items():\n            stream.write_int(key)\n            stream.write_int(len(value))\n            for item in value:\n                item.write(stream)\n\n    def serialize(self) -> dict[int, list[dict[str, float]]]:\n        return {\n            key: [item.serialize() for item in value]\n            for key, value in self.data.items()\n        }\n\n    @staticmethod\n    def deserialize(data: dict[int, list[dict[str, float]]]) -> MapResets:\n        return MapResets(\n            {\n                key: [MapResetData.deserialize(item) for item in value]\n                for key, value in data.items()\n            }\n        )\n\n    def __str__(self) -> str:\n        return f\"MapResets(data={self.data!r})\"\n\n    def __repr__(self) -> str:\n        return str(self)\n"
  },
  {
    "path": "src/bcsfe/core/game/map/outbreaks.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass Outbreak:\n    def __init__(self, cleared: bool):\n        self.cleared = cleared\n\n    @staticmethod\n    def init() -> Outbreak:\n        return Outbreak(False)\n\n    @staticmethod\n    def read(stream: core.Data) -> Outbreak:\n        cleared = stream.read_bool()\n        return Outbreak(cleared)\n\n    def write(self, stream: core.Data):\n        stream.write_bool(self.cleared)\n\n    def serialize(self) -> bool:\n        return self.cleared\n\n    @staticmethod\n    def deserialize(data: bool) -> Outbreak:\n        return Outbreak(data)\n\n    def __repr__(self) -> str:\n        return f\"Outbreak(cleared={self.cleared!r})\"\n\n    def __str__(self) -> str:\n        return f\"Outbreak(cleared={self.cleared!r})\"\n\n\nclass Chapter:\n    def __init__(self, id: int, outbreaks: dict[int, Outbreak]):\n        self.id = id\n        self.outbreaks = outbreaks\n\n    def get_true_id(self) -> int:\n        if self.id < 3:\n            return self.id\n        return self.id - 1\n\n    @staticmethod\n    def init(id: int) -> Chapter:\n        return Chapter(id, {})\n\n    @staticmethod\n    def read(stream: core.Data, id: int) -> Chapter:\n        total = stream.read_int()\n        outbreaks: dict[int, Outbreak] = {}\n        for _ in range(total):\n            outbreak_id = stream.read_int()\n            outbreak = Outbreak.read(stream)\n            outbreaks[outbreak_id] = outbreak\n\n        return Chapter(id, outbreaks)\n\n    def write(self, stream: core.Data):\n        stream.write_int(len(self.outbreaks))\n        for outbreak_id, outbreak in self.outbreaks.items():\n            stream.write_int(outbreak_id)\n            outbreak.write(stream)\n\n    def serialize(self) -> dict[int, Any]:\n        return {\n            outbreak_id: outbreak.serialize()\n            for outbreak_id, outbreak in self.outbreaks.items()\n        }\n\n    @staticmethod\n    def deserialize(data: dict[int, Any], id: int) -> Chapter:\n        return Chapter(\n            id,\n            {\n                outbreak_id: Outbreak.deserialize(outbreak_data)\n                for outbreak_id, outbreak_data in data.items()\n            },\n        )\n\n    def __repr__(self) -> str:\n        return f\"Chapter(id={self.id!r}, outbreaks={self.outbreaks!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass Outbreaks:\n    def __init__(self, chapters: dict[int, Chapter]):\n        self.chapters = chapters\n        self.zombie_event_remaining_time = 0.0\n        self.current_outbreaks: dict[int, Chapter] = {}\n\n    @staticmethod\n    def init() -> Outbreaks:\n        return Outbreaks({})\n\n    @staticmethod\n    def read_chapters(stream: core.Data) -> Outbreaks:\n        total = stream.read_int()\n        chapters: dict[int, Chapter] = {}\n        for _ in range(total):\n            chapter_id = stream.read_int()\n            chapter = Chapter.read(stream, chapter_id)\n            chapters[chapter_id] = chapter\n\n        return Outbreaks(chapters)\n\n    def write_chapters(self, stream: core.Data):\n        stream.write_int(len(self.chapters))\n        for chapter_id, chapter in self.chapters.items():\n            stream.write_int(chapter_id)\n            chapter.write(stream)\n\n    def read_2(self, stream: core.Data):\n        self.zombie_event_remaining_time = stream.read_double()\n\n    def write_2(self, stream: core.Data):\n        stream.write_double(self.zombie_event_remaining_time)\n\n    def read_current_outbreaks(self, stream: core.Data, gv: core.GameVersion):\n        if gv <= 43:\n            total_chapters = stream.read_int()\n            for _ in range(total_chapters):\n                stream.read_int()\n                total_stage = stream.read_int()\n                for _ in range(total_stage):\n                    stream.read_int()\n                    stream.read_bool()\n\n        total = stream.read_int()\n        current_outbreaks: dict[int, Chapter] = {}\n        for _ in range(total):\n            chapter_id = stream.read_int()\n            chapter = Chapter.read(stream, chapter_id)\n            current_outbreaks[chapter_id] = chapter\n\n        self.current_outbreaks = current_outbreaks\n\n    def write_current_outbreaks(self, stream: core.Data, gv: core.GameVersion):\n        if gv <= 43:\n            stream.write_int(0)\n        stream.write_int(len(self.current_outbreaks))\n        for chapter_id, chapter in self.current_outbreaks.items():\n            stream.write_int(chapter_id)\n            chapter.write(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": {\n                chapter_id: chapter.serialize()\n                for chapter_id, chapter in self.chapters.items()\n            },\n            \"zombie_event_remaining_time\": self.zombie_event_remaining_time,\n            \"current_outbreaks\": {\n                chapter_id: chapter.serialize()\n                for chapter_id, chapter in self.current_outbreaks.items()\n            },\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Outbreaks:\n        outbreaks = Outbreaks(\n            {\n                chapter_id: Chapter.deserialize(chapter_data, chapter_id)\n                for chapter_id, chapter_data in data.get(\"chapters\", {}).items()\n            }\n        )\n        outbreaks.zombie_event_remaining_time = data.get(\n            \"zombie_event_remaining_time\", 0.0\n        )\n        outbreaks.current_outbreaks = {\n            chapter_id: Chapter.deserialize(chapter_data, chapter_id)\n            for chapter_id, chapter_data in data.get(\"current_outbreaks\", {}).items()\n        }\n\n        return outbreaks\n\n    def __repr__(self) -> str:\n        return f\"Outbreaks(chapters={self.chapters!r}, zombie_event_remaining_time={self.zombie_event_remaining_time!r}, current_outbreaks={self.current_outbreaks!r})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def get_chapter_from_true_id(self, true_id: int) -> Chapter | None:\n        if true_id < 3:\n            return self.chapters.get(true_id)\n        return self.chapters.get(true_id + 1)\n\n    def get_current_chapter_from_true_id(self, true_id: int) -> Chapter | None:\n        if true_id < 3:\n            return self.current_outbreaks.get(true_id)\n        return self.current_outbreaks.get(true_id + 1)\n\n    def clear_outbreak(self, chapter_id: int, stage_id: int, clear: bool):\n        chapter = self.get_chapter_from_true_id(chapter_id)\n        if chapter is not None:\n            stage = chapter.outbreaks.get(stage_id)\n            if stage is not None:\n                stage.cleared = clear\n        if clear:\n            chapter = self.get_current_chapter_from_true_id(chapter_id)\n            if chapter is not None:\n                stage = chapter.outbreaks.get(stage_id)\n                if stage is not None:\n                    stage.cleared = False\n\n    @staticmethod\n    def edit_outbreaks(save_file: core.SaveFile):\n        outbreaks = save_file.outbreaks\n        chapters = outbreaks.chapters\n        if not chapters:\n            color.ColoredText.localize(\"no_valid_outbreaks\")\n            return\n\n        options = [\"clear\", \"unclear\"]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"clear_unclear_outbreaks\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n\n        clear = choice == 0\n\n        selected_ids = core.StoryChapters.select_story_chapters(\n            save_file, [chapter.get_true_id() for chapter in chapters.values()]\n        )\n        if not selected_ids:\n            return\n\n        choice = core.StoryChapters.get_per_chapter(selected_ids)\n        if choice is None:\n            return\n        if choice == 0:\n            for chapter_id in selected_ids:\n                stages = core.StoryChapters.select_stages(save_file, chapter_id)\n                if not stages:\n                    continue\n                for stage in stages:\n                    outbreaks.clear_outbreak(chapter_id, stage, clear)\n        else:\n            stages = core.StoryChapters.select_stages(save_file, 0)\n            if not stages:\n                return\n            for stage in stages:\n                for chapter_id in selected_ids:\n                    outbreaks.clear_outbreak(chapter_id, stage, clear)\n\n        if clear:\n            color.ColoredText.localize(\"clear_outbreaks_success\")\n        else:\n            color.ColoredText.localize(\"unclear_outbreaks_success\")\n"
  },
  {
    "path": "src/bcsfe/core/game/map/story.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass Stage:\n    def __init__(self, clear_times: int):\n        self.clear_times = clear_times\n        self.treasure = 0\n        self.itf_timed_score = 0\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read_clear_times(stream: core.Data) -> Stage:\n        return Stage(stream.read_int())\n\n    def read_treasure(self, stream: core.Data):\n        self.treasure = stream.read_int()\n\n    def read_itf_timed_score(self, stream: core.Data):\n        self.itf_timed_score = stream.read_int()\n\n    def write_clear_times(self, stream: core.Data):\n        stream.write_int(self.clear_times)\n\n    def write_treasure(self, stream: core.Data):\n        stream.write_int(self.treasure)\n\n    def write_itf_timed_score(self, stream: core.Data):\n        stream.write_int(self.itf_timed_score)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"clear_times\": self.clear_times,\n            \"treasure\": self.treasure,\n            \"itf_timed_score\": self.itf_timed_score,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Stage:\n        stage = Stage(data.get(\"clear_times\", 0))\n        stage.treasure = data.get(\"treasure\", 0)\n        stage.itf_timed_score = data.get(\"itf_timed_score\", 0)\n        return stage\n\n    def __repr__(self):\n        return f\"Stage({self.clear_times}, {self.treasure}, {self.itf_timed_score})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def clear_stage(self, clear_amount: int = 1):\n        self.clear_times = clear_amount\n\n    def unclear_stage(self):\n        self.clear_times = 0\n\n    def is_cleared(self) -> bool:\n        return self.clear_times > 0\n\n    def set_treasure(self, treasure: int):\n        self.treasure = treasure\n\n\nclass Chapter:\n    def __init__(self, selected_stage: int):\n        self.selected_stage = selected_stage\n        self.progress = 0\n        self.stages = [Stage.init() for _ in range(51)]\n        self.time_until_treasure_chance = 0\n        self.treasure_chance_duration = 0\n        self.treasure_chance_value = 0\n        self.treasure_chance_stage_id = 0\n        self.treasure_festival_type = 0\n\n    def clear_stage(\n        self,\n        index: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n    ):\n        if overwrite_clear_progress:\n            self.progress = index + 1\n        else:\n            self.progress = max(self.progress, index + 1)\n        self.stages[index].clear_stage(clear_amount)\n\n    def set_treasure(self, stage_id: int, treasure: int):\n        self.stages[stage_id].set_treasure(treasure)\n\n    def is_stage_clear(self, stage_id: int) -> bool:\n        return self.stages[stage_id].is_cleared()\n\n    @staticmethod\n    def init() -> Chapter:\n        return Chapter(0)\n\n    def get_treasure_stages(self) -> list[Stage]:\n        return self.stages[:49]\n\n    def get_valid_treasure_stages(self) -> list[Stage]:\n        return self.stages[:48]\n\n    @staticmethod\n    def read_selected_stage(stream: core.Data) -> Chapter:\n        return Chapter(stream.read_int())\n\n    def read_progress(self, stream: core.Data):\n        self.progress = stream.read_int()\n\n    def read_clear_times(self, stream: core.Data):\n        total_stages = 51\n        self.stages = [Stage.read_clear_times(stream) for _ in range(total_stages)]\n\n    def read_treasure(self, stream: core.Data):\n        for stage in self.get_treasure_stages():\n            stage.read_treasure(stream)\n\n    def read_time_until_treasure_chance(self, stream: core.Data):\n        self.time_until_treasure_chance = stream.read_int()\n\n    def read_treasure_chance_duration(self, stream: core.Data):\n        self.treasure_chance_duration = stream.read_int()\n\n    def read_treasure_chance_value(self, stream: core.Data):\n        self.treasure_chance_value = stream.read_int()\n\n    def read_treasure_chance_stage_id(self, stream: core.Data):\n        self.treasure_chance_stage_id = stream.read_int()\n\n    def read_treasure_festival_type(self, stream: core.Data):\n        self.treasure_festival_type = stream.read_int()\n\n    def read_itf_timed_scores(self, stream: core.Data):\n        for stage in self.stages:\n            stage.read_itf_timed_score(stream)\n\n    def write_selected_stage(self, stream: core.Data):\n        stream.write_int(self.selected_stage)\n\n    def write_progress(self, stream: core.Data):\n        stream.write_int(self.progress)\n\n    def write_clear_times(self, stream: core.Data):\n        for stage in self.stages:\n            stage.write_clear_times(stream)\n\n    def write_treasure(self, stream: core.Data):\n        for stage in self.get_treasure_stages():\n            stage.write_treasure(stream)\n\n    def write_time_until_treasure_chance(self, stream: core.Data):\n        stream.write_int(self.time_until_treasure_chance)\n\n    def write_treasure_chance_duration(self, stream: core.Data):\n        stream.write_int(self.treasure_chance_duration)\n\n    def write_treasure_chance_value(self, stream: core.Data):\n        stream.write_int(self.treasure_chance_value)\n\n    def write_treasure_chance_stage_id(self, stream: core.Data):\n        stream.write_int(self.treasure_chance_stage_id)\n\n    def write_treasure_festival_type(self, stream: core.Data):\n        stream.write_int(self.treasure_festival_type)\n\n    def write_itf_timed_scores(self, stream: core.Data):\n        for stage in self.stages:\n            stage.write_itf_timed_score(stream)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"selected_stage\": self.selected_stage,\n            \"progress\": self.progress,\n            \"stages\": [stage.serialize() for stage in self.stages],\n            \"time_until_treasure_chance\": self.time_until_treasure_chance,\n            \"treasure_chance_duration\": self.treasure_chance_duration,\n            \"treasure_chance_value\": self.treasure_chance_value,\n            \"treasure_chance_stage_id\": self.treasure_chance_stage_id,\n            \"treasure_festival_type\": self.treasure_festival_type,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Chapter:\n        chapter = Chapter(data.get(\"selected_stage\", 0))\n        chapter.progress = data.get(\"progress\", 0)\n        chapter.stages = [Stage.deserialize(stage) for stage in data.get(\"stages\", [])]\n        chapter.time_until_treasure_chance = data.get(\"time_until_treasure_chance\", 0)\n        chapter.treasure_chance_duration = data.get(\"treasure_chance_duration\", 0)\n        chapter.treasure_chance_value = data.get(\"treasure_chance_value\", 0)\n        chapter.treasure_chance_stage_id = data.get(\"treasure_chance_stage_id\", 0)\n        chapter.treasure_festival_type = data.get(\"treasure_festival_type\", 0)\n        return chapter\n\n    def __repr__(self):\n        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})\"\n\n    def __str__(self):\n        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})\"\n\n    def apply_progress(self, progress: int, clear_times: list[int] | None = None):\n        if clear_times is None:\n            clear_times = [1] * progress\n\n        self.progress = progress\n        for i in range(progress + 1, 48):\n            self.stages[i].unclear_stage()\n\n        for i in range(progress):\n            self.stages[i].clear_stage(clear_times[i])\n\n    def clear_chapter(self):\n        self.apply_progress(48)\n\n\nclass StoryChapters:\n    def __init__(self, chapters: list[Chapter]):\n        self.chapters = chapters\n\n    def get_real_chapters(self) -> list[Chapter]:\n        new_chapters: list[Chapter] = []\n        for i, chapter in enumerate(self.chapters):\n            if i == 3:\n                continue\n            new_chapters.append(chapter)\n        return new_chapters\n\n    def clear_stage(\n        self,\n        map: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        chapters: list[Chapter] | None = None,\n    ):\n        if chapters is None:\n            chapters = self.chapters\n        chapters[map].clear_stage(stage, clear_amount, overwrite_clear_progress)\n\n    def set_treasure(self, chapter: int, stage: int, treasure: int):\n        self.chapters[chapter].set_treasure(stage, treasure)\n\n    def is_stage_clear(self, chapter: int, stage: int) -> bool:\n        return self.chapters[chapter].is_stage_clear(stage)\n\n    @staticmethod\n    def init() -> StoryChapters:\n        chapters = [Chapter.init() for _ in range(10)]\n        return StoryChapters(chapters)\n\n    @staticmethod\n    def read(stream: core.Data) -> StoryChapters:\n        total_chapters = 10\n        chapters_l = [\n            Chapter.read_selected_stage(stream) for _ in range(total_chapters)\n        ]\n        chapters = StoryChapters(chapters_l)\n        for chapter in chapters.chapters:\n            chapter.read_progress(stream)\n        for chapter in chapters.chapters:\n            chapter.read_clear_times(stream)\n        for chapter in chapters.chapters:\n            chapter.read_treasure(stream)\n        return chapters\n\n    def read_treasure_festival(self, stream: core.Data):\n        for chapter in self.chapters:\n            chapter.read_time_until_treasure_chance(stream)\n        for chapter in self.chapters:\n            chapter.read_treasure_chance_duration(stream)\n        for chapter in self.chapters:\n            chapter.read_treasure_chance_value(stream)\n        for chapter in self.chapters:\n            chapter.read_treasure_chance_stage_id(stream)\n        for chapter in self.chapters:\n            chapter.read_treasure_festival_type(stream)\n\n    def write(self, stream: core.Data):\n        for chapter in self.chapters:\n            chapter.write_selected_stage(stream)\n        for chapter in self.chapters:\n            chapter.write_progress(stream)\n        for chapter in self.chapters:\n            chapter.write_clear_times(stream)\n        for chapter in self.chapters:\n            chapter.write_treasure(stream)\n\n    def write_treasure_festival(self, stream: core.Data):\n        for chapter in self.chapters:\n            chapter.write_time_until_treasure_chance(stream)\n        for chapter in self.chapters:\n            chapter.write_treasure_chance_duration(stream)\n        for chapter in self.chapters:\n            chapter.write_treasure_chance_value(stream)\n        for chapter in self.chapters:\n            chapter.write_treasure_chance_stage_id(stream)\n        for chapter in self.chapters:\n            chapter.write_treasure_festival_type(stream)\n\n    def read_itf_timed_scores(self, stream: core.Data):\n        # 0: eoc 1\n        # 1: eoc 2\n        # 2: eoc 3\n        # 3: _\n        # 4: itf 1\n        # 5: itf 2\n        # 6: itf 3\n        # 7: cotc 1\n        # 8: cotc 2\n        # 9: cotc 3\n\n        for i, chapter in enumerate(self.chapters):\n            if i > 3 and i < 7:\n                chapter.read_itf_timed_scores(stream)\n\n    def write_itf_timed_scores(self, stream: core.Data):\n        for i, chapter in enumerate(self.chapters):\n            if i > 3 and i < 7:\n                chapter.write_itf_timed_scores(stream)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        chapters = [chapter.serialize() for chapter in self.chapters]\n        return chapters\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> StoryChapters:\n        chapters = StoryChapters([Chapter.deserialize(chapter) for chapter in data])\n        return chapters\n\n    def __repr__(self):\n        return f\"Chapters({self.chapters})\"\n\n    def __str__(self):\n        return f\"Chapters({self.chapters})\"\n\n    @staticmethod\n    def clear_tutorial(save_file: core.SaveFile):\n        save_file.tutorial_state = max(save_file.tutorial_state, 1)\n        save_file.koreaSuperiorTreasureState = max(\n            save_file.koreaSuperiorTreasureState, 2\n        )\n        save_file.ui6 = max(save_file.ui6, 1)\n        new_length = len(save_file.new_dialogs_2)\n        if new_length < 6:\n            save_file.new_dialogs_2.extend([0] * (6 - new_length))\n\n        save_file.new_dialogs_2[1] = max(save_file.new_dialogs_2[1], 2)\n        save_file.new_dialogs_2[5] = max(save_file.new_dialogs_2[5], 2)\n        if save_file.story.chapters[0].stages[0].clear_times == 0:\n            save_file.story.clear_stage(0, 0)\n\n    @staticmethod\n    def get_chapter_names(\n        save_file: core.SaveFile, chapter_ids: list[int] | None = None\n    ) -> list[str] | None:\n        if chapter_ids is None:\n            chapter_ids = [0, 1, 2, 3, 4, 5, 6, 7, 8]\n\n        chapter_names: list[str] = []\n        localizable = core.core_data.get_localizable(save_file)\n        eoc_name = localizable.get(\"everyplay_mapname_J\")\n        itf_name = localizable.get(\"everyplay_mapname_W\")\n        cotc_name = localizable.get(\"everyplay_mapname_P\")\n        if eoc_name is None or itf_name is None or cotc_name is None:\n            return None\n\n        for chapter_id in chapter_ids:\n            if chapter_id < 3:\n                chapter_names.append(eoc_name.replace(\"%d\", str(chapter_id + 1)))\n            elif chapter_id < 6:\n                chapter_names.append(itf_name.replace(\"%d\", str(chapter_id - 2)))\n            else:\n                chapter_names.append(cotc_name.replace(\"%d\", str(chapter_id - 5)))\n\n        return chapter_names\n\n    @staticmethod\n    def select_story_chapters(\n        save_file: core.SaveFile, chapters: list[int] | None = None\n    ) -> list[int] | None:\n        chapter_names = StoryChapters.get_chapter_names(save_file, chapters)\n\n        if chapter_names is None:\n            return None\n\n        selected_chapters, _ = dialog_creator.ChoiceInput.from_reduced(\n            chapter_names, dialog=\"select_story_chapters\"\n        ).multiple_choice(localized_options=False)\n\n        return selected_chapters\n\n    @staticmethod\n    def get_selected_chapter_progress(max_stages: int = 48) -> int | None:\n        progress = dialog_creator.IntInput(\n            min=0, max=max_stages\n        ).get_input_locale_while(\"edit_chapter_progress_all\", {\"max\": max_stages})\n        if progress is None:\n            return None\n\n        return progress\n\n    @staticmethod\n    def edit_chapter_progress(\n        save_file: core.SaveFile,\n        chapter_id: int,\n        chapter_name: str,\n        clear_amount: int,\n        clear_amount_choose: int,\n    ) -> bool:\n        max_stages = 48\n        chapter = save_file.story.get_real_chapters()[chapter_id]\n        progress = dialog_creator.IntInput(\n            min=0, max=max_stages\n        ).get_input_locale_while(\n            \"edit_chapter_progress\",\n            {\"max\": max_stages, \"chapter_name\": chapter_name},\n        )\n        if progress is None:\n            return False\n        clear_amounts = [1] * progress\n        if clear_amount_choose == 0:\n            clear_amount2 = core.EventChapters.ask_clear_amount()\n            if clear_amount2 is None:\n                return False\n            clear_amounts = [clear_amount2] * progress\n        elif clear_amount_choose == 1:\n            clear_amounts = [clear_amount] * progress\n        elif clear_amount_choose == 2:\n            for i in range(progress):\n                StoryChapters.print_current_stage(save_file, chapter_id, i)\n                clear_amount2 = core.EventChapters.ask_clear_amount()\n                if clear_amount2 is None:\n                    return False\n                clear_amounts[i] = clear_amount2\n\n        chapter.apply_progress(progress, clear_amounts)\n        return progress != 0\n\n    @staticmethod\n    def convert_stage_id(index: int) -> int:\n        if index == 46:\n            return 46\n        if index == 47:\n            return 47\n        index = 45 - index\n        return index\n\n    @staticmethod\n    def ask_clear_count() -> int | None:\n        clear_count = dialog_creator.IntInput(\n            min=0,\n            max=core.core_data.max_value_manager.get(\"stage_clear_count\"),\n        ).get_input_locale_while(\"edit_stage_clear_count\", {})\n\n        return clear_count\n\n    @staticmethod\n    def ask_if_individual_clear_counts() -> bool | None:\n        options = [\"individual_clear_counts\", \"all_clear_counts\"]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"individual_clear_counts_dialog\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return None\n        choice -= 1\n        return choice == 0\n\n    @staticmethod\n    def edit_stage_clear_count(\n        save_file: core.SaveFile, chapter_id: int, stage_id: int\n    ):\n        chapter = save_file.story.get_real_chapters()[chapter_id]\n        stage = chapter.stages[stage_id]\n        clear_count = StoryChapters.ask_clear_count()\n        if clear_count is None:\n            return\n        stage.clear_times = clear_count\n\n    def clear_previous_chapters(self, chapter_id: int):\n        chapters = self.get_real_chapters()\n        \"\"\"\n        0: eoc 1\n        1: eoc 2 - requires eoc 1\n        2: eoc 3 - requires eoc 1 + eoc 2\n        3: itf 1 - requires eoc 1\n        4: itf 2 - requires eoc 1 + itf 1\n        5: itf 3 - requires eoc 1 + itf 1 + itf 2\n        6: cotc 1 - requires eoc 1 + itf 1\n        7: cotc 2 - requires eoc 1 + itf 1 + cotc 1\n        8: cotc 3 - requires eoc 1 + itf 1 + cotc 1 + cotc 2\n\n        \"\"\"\n        if chapter_id == 1:  # eoc 2\n            chapters[0].clear_chapter()\n        elif chapter_id == 2:  # eoc 3\n            chapters[0].clear_chapter()\n            chapters[1].clear_chapter()\n        elif chapter_id == 3:  # itf 1\n            chapters[0].clear_chapter()\n        elif chapter_id == 4:  # itf 2\n            chapters[0].clear_chapter()\n            chapters[3].clear_chapter()\n        elif chapter_id == 5:  # itf 3\n            chapters[0].clear_chapter()\n            chapters[3].clear_chapter()\n            chapters[4].clear_chapter()\n        elif chapter_id == 6:  # cotc 1\n            chapters[0].clear_chapter()\n            chapters[3].clear_chapter()\n        elif chapter_id == 7:  # cotc 2\n            chapters[0].clear_chapter()\n            chapters[3].clear_chapter()\n            chapters[6].clear_chapter()\n        elif chapter_id == 8:  # cotc 3\n            chapters[0].clear_chapter()\n            chapters[3].clear_chapter()\n            chapters[6].clear_chapter()\n            chapters[7].clear_chapter()\n\n    @staticmethod\n    def print_current_chapter(save_file: core.SaveFile, chapter_id: int):\n        chapter_names = StoryChapters.get_chapter_names(save_file)\n        if chapter_names is None:\n            return\n        chapter_name = chapter_names[chapter_id]\n        color.ColoredText.localize(\"current_chapter\", chapter_name=chapter_name)\n\n    @staticmethod\n    def print_current_treasure_group(\n        save_file: core.SaveFile, chapter_id: int, treasure_group_id: int\n    ):\n        chapter_type = StoryChapters.get_chapter_type_from_index(chapter_id)\n\n        treasure_group_names = TreasureGroupNames(\n            save_file, chapter_type\n        ).treasure_group_names\n        if treasure_group_names is None:\n            return\n        treasure_group_name = treasure_group_names[treasure_group_id]\n        color.ColoredText.localize(\n            \"current_treasure_group\", treasure_group_name=treasure_group_name\n        )\n\n    @staticmethod\n    def clear_story(save_file: core.SaveFile):\n        story = save_file.story\n        story.edit_chapters(\n            save_file,\n        )\n\n    def edit_chapters(self, save_file: core.SaveFile):\n        chapters = self.get_real_chapters()\n        names = StoryChapters.get_chapter_names(save_file)\n        if names is None:\n            return\n\n        map_choices = StoryChapters.select_story_chapters(save_file)\n        if not map_choices:\n            return\n\n        clear_type_choice = dialog_creator.ChoiceInput.from_reduced(\n            [\"clear_whole_chapters\", \"clear_specific_stages\"],\n            dialog=\"select_clear_type\",\n            single_choice=True,\n        ).single_choice()\n        if clear_type_choice is None:\n            return\n        clear_type_choice -= 1\n\n        modify_clear_amounts = dialog_creator.YesNoInput().get_input_once(\n            \"modify_clear_amounts\"\n        )\n        if modify_clear_amounts is None:\n            return\n        clear_amount = 1\n        clear_amount_type = -1\n        if modify_clear_amounts:\n            if len(map_choices) == 1:\n                clear_amount_type = 0\n            else:\n                options = [\"clear_amount_chapter\", \"clear_amount_all\"]\n                if clear_type_choice == 1:\n                    options.append(\"clear_amount_stages\")\n                clear_amount_type = dialog_creator.ChoiceInput.from_reduced(\n                    options, dialog=\"select_clear_amount_type\", single_choice=True\n                ).single_choice()\n                if clear_amount_type is None:\n                    return\n                clear_amount_type -= 1\n\n            if clear_amount_type == 1:\n                clear_amount = core.EventChapters.ask_clear_amount()\n                if clear_amount is None:\n                    return\n\n        for id in map_choices:\n            stage_names = StageNames(\n                save_file, chapter=str(self.get_chapter_type_from_index(id))\n            )\n            stage_names = stage_names.stage_names\n            if stage_names is None:\n                return\n\n            new_stage_names: list[str] = []\n            for i in range(48):\n                index_stage_id = StoryChapters.convert_stage_id(i)\n                new_stage_names.append(stage_names[index_stage_id])\n            stage_names = new_stage_names\n            map_name = names[id]\n            color.ColoredText.localize(\"current_sol_chapter\", name=map_name, id=id)\n            if clear_type_choice:\n                stages = core.EventChapters.ask_stages_stage_names(stage_names)\n                if stages is None:\n                    return\n            else:\n                stages = list(range(48))\n\n            if clear_amount_type == 0:\n                clear_amount = core.EventChapters.ask_clear_amount()\n                if clear_amount is None:\n                    return\n\n            could_unclear_stages = False\n\n            if chapters[id].progress > max(stages) + 1:\n                could_unclear_stages = True\n\n            for stage in range(max(stages) + 1, 48):\n                if chapters[id].stages[stage].clear_times:\n                    could_unclear_stages = True\n\n            if could_unclear_stages:\n                unclear_other_stages = dialog_creator.YesNoInput().get_input_once(\n                    \"unclear_other_stages\"\n                )\n                if unclear_other_stages is None:\n                    return\n            else:\n                unclear_other_stages = False\n\n            if unclear_other_stages:\n                chapters[id].progress = 0\n                for stage in range(max(stages), 48):\n                    chapters[id].stages[stage].clear_times = 0\n\n            for stage in stages:\n                if clear_amount_type == 2:\n                    stage_name = stage_names[stage]\n                    color.ColoredText.localize(\n                        \"current_sol_stage\", name=stage_name, id=stage\n                    )\n                if clear_amount_type == 2:\n                    clear_amount = core.EventChapters.ask_clear_amount()\n                    if clear_amount is None:\n                        return\n                self.clear_stage(\n                    id,\n                    stage,\n                    overwrite_clear_progress=True,\n                    clear_amount=clear_amount,\n                    chapters=chapters,\n                )\n\n        color.ColoredText.localize(\"map_chapters_edited\")\n\n    @staticmethod\n    def ask_treasure_level(save_file: core.SaveFile) -> int | None:\n        treasure_text = core.core_data.get_treasure_text(save_file).treasure_text\n        if treasure_text is None:\n            return None\n        if len(treasure_text) < 3:\n            return None\n        options = [\n            \"no_treasure\",\n            treasure_text[0],\n            treasure_text[1],\n            treasure_text[2],\n            \"custom_treasure_level\",\n        ]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"treasure_level_dialog\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return None\n        choice -= 1\n\n        max_treasure_level = core.core_data.max_value_manager.get(\"treasure_level\")\n\n        if choice == 4:\n            treasure_level = dialog_creator.IntInput(\n                min=0, max=max_treasure_level\n            ).get_input_locale_while(\"custom_treasure_level_dialog\", {})\n            if treasure_level is None:\n                return None\n            return treasure_level\n\n        return choice\n\n    @staticmethod\n    def get_per_chapter(chapters: list[int]) -> int | None:\n        if len(chapters) == 1:\n            return 0\n\n        options = [\"per_chapter\", \"all_selected_chapters\"]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"edit_per_chapter\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return None\n        choice -= 1\n        return choice\n\n    @staticmethod\n    def edit_treasures_whole_chapters(save_file: core.SaveFile, chapters: list[int]):\n        choice = StoryChapters.get_per_chapter(chapters)\n        if choice is None:\n            return\n\n        if choice == 0:\n            for chapter_id in chapters:\n                StoryChapters.print_current_chapter(save_file, chapter_id)\n                chapter = save_file.story.get_real_chapters()[chapter_id]\n                treasure_level = StoryChapters.ask_treasure_level(save_file)\n                if treasure_level is None:\n                    return\n                for stage in chapter.get_valid_treasure_stages():\n                    stage.set_treasure(treasure_level)\n        else:\n            treasure_level = StoryChapters.ask_treasure_level(save_file)\n            if treasure_level is None:\n                return\n            for chapter_id in chapters:\n                chapter = save_file.story.get_real_chapters()[chapter_id]\n                for stage in chapter.get_valid_treasure_stages():\n                    stage.set_treasure(treasure_level)\n\n    @staticmethod\n    def get_chapter_type_from_index(index: int) -> int:\n        if index < 3:\n            return 0\n        if index < 6:\n            return 1\n        return 2\n\n    @staticmethod\n    def select_stages(save_file: core.SaveFile, chapter_id: int) -> list[int] | None:\n        options = [\"select_stage_by_id\", \"select_stage_by_name\"]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"select_stage_dialog\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return None\n        choice -= 1\n\n        if choice == 0:\n            stage_ids = dialog_creator.RangeInput(48, 1).get_input_locale(\n                \"select_stage_id\", {}\n            )\n            if stage_ids is None:\n                return None\n            stage_ids = [stage_id - 1 for stage_id in stage_ids]\n            return stage_ids\n\n        chapter_type = StoryChapters.get_chapter_type_from_index(chapter_id)\n        stage_names = StageNames(save_file, str(chapter_type)).stage_names\n        if not stage_names:\n            return None\n        new_stage_names: list[str] = []\n        for i in range(48):\n            index_stage_id = StoryChapters.convert_stage_id(i)\n            new_stage_names.append(stage_names[index_stage_id])\n        selected_stages, _ = dialog_creator.ChoiceInput.from_reduced(\n            new_stage_names, dialog=\"select_stages_name\"\n        ).multiple_choice(localized_options=False)\n\n        if not selected_stages:\n            return None\n\n        return selected_stages\n\n    @staticmethod\n    def edit_treasures_individual_stages(save_file: core.SaveFile, chapters: list[int]):\n        choice = StoryChapters.get_per_chapter(chapters)\n        if choice is None:\n            return\n        if choice == 0:\n            for chapter_id in chapters:\n                StoryChapters.print_current_chapter(save_file, chapter_id)\n                chapter = save_file.story.get_real_chapters()[chapter_id]\n                stage_ids = StoryChapters.select_stages(save_file, chapter_id)\n                if stage_ids is None:\n                    return\n                treasure_level = StoryChapters.ask_treasure_level(save_file)\n                if treasure_level is None:\n                    return\n                for stage_id in stage_ids:\n                    real_stage_id = StoryChapters.convert_stage_id(stage_id)\n                    chapter.set_treasure(real_stage_id, treasure_level)\n        else:\n            stage_ids = StoryChapters.select_stages(save_file, 0)\n            if stage_ids is None:\n                return\n            treasure_level = StoryChapters.ask_treasure_level(save_file)\n            if treasure_level is None:\n                return\n            for chapter_id in chapters:\n                chapter = save_file.story.get_real_chapters()[chapter_id]\n                for stage_id in stage_ids:\n                    real_stage_id = StoryChapters.convert_stage_id(stage_id)\n                    chapter.set_treasure(real_stage_id, treasure_level)\n\n    @staticmethod\n    def edit_treasures_groups(save_file: core.SaveFile, chapters: list[int]):\n        for chapter_id in chapters:\n            StoryChapters.print_current_chapter(save_file, chapter_id)\n            chapter = save_file.story.get_real_chapters()[chapter_id]\n            chapter_type = StoryChapters.get_chapter_type_from_index(chapter_id)\n            treasure_group_data = TreasureGroupData(\n                save_file, chapter_type\n            ).treasure_group_data\n            treasure_group_names = TreasureGroupNames(\n                save_file, chapter_type\n            ).treasure_group_names\n            if not treasure_group_data or not treasure_group_names:\n                return\n            treasure_group_names_new: list[str] = []\n            for i in range(len(treasure_group_data)):\n                treasure_group_names_new.append(treasure_group_names[i])\n\n            selected_treasure_groups, _ = dialog_creator.ChoiceInput.from_reduced(\n                treasure_group_names_new, dialog=\"select_treasure_groups\"\n            ).multiple_choice(localized_options=False)\n\n            if not selected_treasure_groups:\n                return\n\n            options = [\"group_individual\", \"group_all_at_once\"]\n            choice = dialog_creator.ChoiceInput.from_reduced(\n                options, dialog=\"select_treasure_groups_individual\"\n            ).single_choice()\n            if choice is None:\n                return\n            choice -= 1\n\n            if choice == 0:\n                for treasure_group_id in selected_treasure_groups:\n                    StoryChapters.print_current_treasure_group(\n                        save_file, chapter_id, treasure_group_id\n                    )\n                    treasure_level = StoryChapters.ask_treasure_level(save_file)\n                    if treasure_level is None:\n                        return\n                    treasure_group = treasure_group_data[treasure_group_id]\n                    for stage_id in treasure_group:\n                        chapter.set_treasure(stage_id, treasure_level)\n\n            else:\n                treasure_level = StoryChapters.ask_treasure_level(save_file)\n                if treasure_level is None:\n                    return\n\n                for treasure_group_id in selected_treasure_groups:\n                    treasure_group = treasure_group_data[treasure_group_id]\n                    for stage_id in treasure_group:\n                        chapter.set_treasure(stage_id, treasure_level)\n\n    @staticmethod\n    def edit_treasures(save_file: core.SaveFile):\n        selected_chapters = StoryChapters.select_story_chapters(save_file)\n        if not selected_chapters:\n            return\n        options = [\"whole_chapters\", \"individual_stages\", \"treasure_groups\"]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"treasure_dialog\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n\n        if choice == 0:\n            StoryChapters.edit_treasures_whole_chapters(save_file, selected_chapters)\n        elif choice == 1:\n            StoryChapters.edit_treasures_individual_stages(save_file, selected_chapters)\n        elif choice == 2:\n            StoryChapters.edit_treasures_groups(save_file, selected_chapters)\n\n        color.ColoredText.localize(\"treasures_edited\")\n\n    @staticmethod\n    def edit_itf_timed_scores(save_file: core.SaveFile):\n        selected_chapters = StoryChapters.select_story_chapters(\n            save_file, chapters=[3, 4, 5]\n        )\n        if not selected_chapters:\n            return\n        options = [\"whole_chapters\", \"individual_stages\"]\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            options, dialog=\"itf_timed_scores_dialog\", single_choice=True\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n\n        selected_chapters = [chapter_id + 3 for chapter_id in selected_chapters]\n\n        if choice == 0:\n            StoryChapters.edit_itf_timed_scores_whole_chapters(\n                save_file, selected_chapters\n            )\n        elif choice == 1:\n            StoryChapters.edit_itf_timed_scores_individual_stages(\n                save_file, selected_chapters\n            )\n\n        color.ColoredText.localize(\"itf_timed_scores_edited\")\n\n    @staticmethod\n    def edit_itf_timed_scores_whole_chapters(\n        save_file: core.SaveFile, chapters: list[int]\n    ):\n        choice = StoryChapters.get_per_chapter(chapters)\n        if choice is None:\n            return\n\n        if choice == 0:\n            for chapter_id in chapters:\n                print(chapter_id)\n                StoryChapters.print_current_chapter(save_file, chapter_id)\n                chapter = save_file.story.get_real_chapters()[chapter_id]\n                score = dialog_creator.IntInput(\n                    min=0,\n                    max=core.core_data.max_value_manager.get(\"itf_timed_score\"),\n                ).get_input_locale_while(\"itf_timed_score_dialog\", {})\n                if score is None:\n                    return\n                for stage in chapter.get_valid_treasure_stages():\n                    stage.itf_timed_score = score\n        else:\n            score = dialog_creator.IntInput(\n                min=0,\n                max=core.core_data.max_value_manager.get(\"itf_timed_score\"),\n            ).get_input_locale_while(\"itf_timed_score_dialog\", {})\n            if score is None:\n                return\n            for chapter_id in chapters:\n                chapter = save_file.story.get_real_chapters()[chapter_id]\n                for stage in chapter.get_valid_treasure_stages():\n                    stage.itf_timed_score = score\n\n    @staticmethod\n    def print_current_stage(save_file: core.SaveFile, chapter_id: int, stage_id: int):\n        chapter_names = StoryChapters.get_chapter_names(save_file)\n        if chapter_names is None:\n            return\n        chapter_name = chapter_names[chapter_id]\n        chapter_type = StoryChapters.get_chapter_type_from_index(chapter_id)\n        stage_names = StageNames(save_file, str(chapter_type)).stage_names\n        if stage_names is None:\n            return\n        stage_id = StoryChapters.convert_stage_id(stage_id)\n        stage_name = stage_names[stage_id]\n        color.ColoredText.localize(\n            \"current_stage\", chapter_name=chapter_name, stage_name=stage_name\n        )\n\n    @staticmethod\n    def edit_itf_timed_scores_individual_stages(\n        save_file: core.SaveFile, chapters: list[int]\n    ):\n        choice = StoryChapters.get_per_chapter(chapters)\n        if choice is None:\n            return\n        options = [\"individual_stages\", \"all_selected_stages\"]\n        choice2 = dialog_creator.ChoiceInput.from_reduced(\n            options,\n            dialog=\"itf_timed_scores_individual_dialog\",\n            single_choice=True,\n        ).single_choice()\n        if choice2 is None:\n            return\n        choice2 -= 1\n\n        if choice == 0:\n            for chapter_id in chapters:\n                StoryChapters.print_current_chapter(save_file, chapter_id)\n                chapter = save_file.story.get_real_chapters()[chapter_id]\n                stage_ids = StoryChapters.select_stages(save_file, chapter_id)\n                if stage_ids is None:\n                    return\n                if choice2 == 0:\n                    for stage_id in stage_ids:\n                        StoryChapters.print_current_stage(\n                            save_file, chapter_id, stage_id\n                        )\n\n                        score = dialog_creator.IntInput(\n                            min=0,\n                            max=core.core_data.max_value_manager.get(\"itf_timed_score\"),\n                        ).get_input_locale_while(\"itf_timed_score_dialog\", {})\n                        if score is None:\n                            return\n                        chapter.stages[stage_id].itf_timed_score = score\n                elif choice2 == 1:\n                    score = dialog_creator.IntInput(\n                        min=0,\n                        max=core.core_data.max_value_manager.get(\"itf_timed_score\"),\n                    ).get_input_locale_while(\"itf_timed_score_dialog\", {})\n                    if score is None:\n                        return\n                    for stage_id in stage_ids:\n                        chapter.stages[stage_id].itf_timed_score = score\n        else:\n            stage_ids = StoryChapters.select_stages(save_file, 3)\n            if stage_ids is None:\n                return\n            if choice2 == 0:\n                for stage_id in stage_ids:\n                    StoryChapters.print_current_stage(save_file, 3, stage_id)\n                    score = dialog_creator.IntInput(\n                        min=0,\n                        max=core.core_data.max_value_manager.get(\"itf_timed_score\"),\n                    ).get_input_locale_while(\"itf_timed_score_dialog\", {})\n                    if score is None:\n                        return\n                    for chapter_id in chapters:\n                        chapter = save_file.story.get_real_chapters()[chapter_id]\n                        chapter.stages[stage_id].itf_timed_score = score\n            elif choice2 == 1:\n                score = dialog_creator.IntInput(\n                    min=0,\n                    max=core.core_data.max_value_manager.get(\"itf_timed_score\"),\n                ).get_input_locale_while(\"itf_timed_score_dialog\", {})\n                if score is None:\n                    return\n                for chapter_id in chapters:\n                    chapter = save_file.story.get_real_chapters()[chapter_id]\n                    for stage_id in stage_ids:\n                        chapter.stages[stage_id].itf_timed_score = score\n\n\nclass StageNames:\n    def __init__(self, save_file: core.SaveFile, chapter: str, max_stages: int = 48):\n        self.save_file = save_file\n        self.chapter = chapter\n        self.max_stages = max_stages\n        self.stage_names = self.get_stage_names()\n\n    def get_file_name(self) -> str:\n        if self.chapter.isdigit():\n            return (\n                f\"StageName{self.chapter}_{core.core_data.get_lang(self.save_file)}.csv\"\n            )\n        return f\"StageName_{self.chapter}_{core.core_data.get_lang(self.save_file)}.csv\"\n\n    def get_stage_names(self) -> list[str] | None:\n        file_name = self.get_file_name()\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        file = gdg.download(\"resLocal\", file_name)\n        if file is None:\n            return None\n        csv = core.CSV(\n            file,\n            delimiter=core.Delimeter.from_country_code_res(self.save_file.cc),\n        )\n        stage_names: list[str] = []\n        if self.chapter.isdigit():\n            for row in csv:\n                stage_names.append(row[0].to_str())\n        else:\n            for row in csv:\n                for value in row:\n                    stage_names.append(value.to_str())\n        return stage_names[: self.max_stages]\n\n    def get_stage_name(self, stage_id: int) -> str | None:\n        if self.stage_names is None:\n            return None\n        return self.stage_names[stage_id]\n\n\nclass TreasureText:\n    def __init__(self, save_file: core.SaveFile):\n        self.save_file = save_file\n        self.treasure_text = self.get_treasure_text()\n\n    def get_tt_file_name(self) -> str:\n        return f\"Treasure2_{core.core_data.get_lang(self.save_file)}.csv\"\n\n    def get_treasure_text(self) -> list[str] | None:\n        file_name = self.get_tt_file_name()\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        file = gdg.download(\"resLocal\", file_name)\n        if file is None:\n            return None\n        csv = core.CSV(\n            file,\n            delimiter=core.Delimeter.from_country_code_res(self.save_file.cc),\n        )\n        treasure_text: list[str] = []\n        for row in csv:\n            treasure_text.append(row[0].to_str())\n        return treasure_text\n\n\nclass TreasureGroupData:\n    def __init__(self, save_file: core.SaveFile, chapter_type: int):\n        self.save_file = save_file\n        self.chapter_type = chapter_type\n        self.treasure_group_data = self.get_treasure_group_data()\n\n    def get_tgd_file_name(self) -> str:\n        if self.chapter_type == 0:\n            return \"treasureData0.csv\"\n        if self.chapter_type == 1:\n            return \"treasureData1.csv\"\n        if self.chapter_type == 2:\n            return \"treasureData2_0.csv\"\n        return \"\"\n\n    def get_treasure_group_data(self) -> list[list[int]] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        file = gdg.download(\"DataLocal\", self.get_tgd_file_name())\n        if file is None:\n            return None\n        csv = core.CSV(file)\n        treasure_group_data: list[list[int]] = []\n        for row in csv.lines[11:22]:\n            treasure_group_data.append(\n                [value.to_int() for value in row if value.to_int() != -1]\n            )\n\n        return treasure_group_data\n\n\nclass TreasureGroupNames:\n    def __init__(self, save_file: core.SaveFile, chapter_type: int):\n        self.save_file = save_file\n        self.chapter_type = chapter_type\n        self.treasure_group_names = self.get_treasure_group_names()\n\n    def get_tgn_file_name(self) -> str:\n        lang = core.core_data.get_lang(self.save_file)\n        if self.chapter_type == 0:\n            return f\"Treasure3_0_{lang}.csv\"\n        if self.chapter_type == 1:\n            return f\"Treasure3_1_{lang}.csv\"\n        if self.chapter_type == 2:\n            return f\"Treasure3_2_0_{lang}.csv\"\n        return \"\"\n\n    def get_treasure_group_names(self) -> list[str] | None:\n        gdg = core.core_data.get_game_data_getter(self.save_file)\n        file = gdg.download(\"resLocal\", self.get_tgn_file_name())\n        if file is None:\n            return None\n        csv = core.CSV(\n            file,\n            delimiter=core.Delimeter.from_country_code_res(self.save_file.cc),\n        )\n        treasure_group_names: list[str] = []\n        for row in csv:\n            treasure_group_names.append(row[0].to_str())\n        return treasure_group_names\n"
  },
  {
    "path": "src/bcsfe/core/game/map/timed_score.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\n\n\nclass Stage:\n    def __init__(self, score: int):\n        self.score = score\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read(stream: core.Data) -> Stage:\n        return Stage(stream.read_int())\n\n    def write(self, stream: core.Data):\n        stream.write_int(self.score)\n\n    def serialize(self) -> int:\n        return self.score\n\n    @staticmethod\n    def deserialize(data: int) -> Stage:\n        return Stage(data)\n\n    def __repr__(self) -> str:\n        return f\"Stage(score={self.score})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass SubChapter:\n    def __init__(self, stages: list[Stage]):\n        self.stages = stages\n\n    @staticmethod\n    def init(total_stages: int) -> SubChapter:\n        return SubChapter([Stage.init() for _ in range(total_stages)])\n\n    @staticmethod\n    def read(stream: core.Data, total_stages: int) -> SubChapter:\n        stages: list[Stage] = []\n        for _ in range(total_stages):\n            stages.append(Stage.read(stream))\n        return SubChapter(stages)\n\n    def write(self, stream: core.Data):\n        for stage in self.stages:\n            stage.write(stream)\n\n    def serialize(self) -> list[int]:\n        return [stage.serialize() for stage in self.stages]\n\n    @staticmethod\n    def deserialize(data: list[int]) -> SubChapter:\n        return SubChapter([Stage.deserialize(stage) for stage in data])\n\n    def __repr__(self) -> str:\n        return f\"SubChapter(stages={self.stages})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass SubChapterStars:\n    def __init__(self, sub_chapters: list[SubChapter]):\n        self.sub_chapters = sub_chapters\n\n    @staticmethod\n    def init(total_stages: int, total_stars: int) -> SubChapterStars:\n        return SubChapterStars(\n            [SubChapter.init(total_stages) for _ in range(total_stars)]\n        )\n\n    @staticmethod\n    def read(\n        stream: core.Data,\n        total_stages: int,\n        total_stars: int,\n    ) -> SubChapterStars:\n        sub_chapters: list[SubChapter] = []\n        for _ in range(total_stars):\n            sub_chapters.append(SubChapter.read(stream, total_stages))\n        return SubChapterStars(sub_chapters)\n\n    def write(self, stream: core.Data):\n        for sub_chapter in self.sub_chapters:\n            sub_chapter.write(stream)\n\n    def serialize(self) -> list[list[int]]:\n        return [sub_chapter.serialize() for sub_chapter in self.sub_chapters]\n\n    @staticmethod\n    def deserialize(data: list[list[int]]) -> SubChapterStars:\n        return SubChapterStars(\n            [SubChapter.deserialize(sub_chapter) for sub_chapter in data]\n        )\n\n    def __repr__(self) -> str:\n        return f\"SubChapterStars(sub_chapters={self.sub_chapters})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n\nclass TimedScoreChapters:\n    def __init__(self, sub_chapters: list[SubChapterStars]):\n        self.sub_chapters = sub_chapters\n\n    @staticmethod\n    def init(gv: core.GameVersion) -> TimedScoreChapters:\n        if gv < 20:\n            return TimedScoreChapters([])\n        if gv <= 33:\n            total_subchapters = 50\n            total_stages = 12\n            total_stars = 3\n        elif gv <= 34:\n            total_subchapters = 0\n            total_stages = 12\n            total_stars = 3\n        else:\n            total_subchapters = 0\n            total_stages = 0\n            total_stars = 0\n        return TimedScoreChapters(\n            [\n                SubChapterStars.init(total_stages, total_stars)\n                for _ in range(total_subchapters)\n            ]\n        )\n\n    @staticmethod\n    def read(stream: core.Data, gv: core.GameVersion) -> TimedScoreChapters:\n        if gv < 20:\n            return TimedScoreChapters([])\n        if gv <= 33:\n            total_subchapters = 50\n            total_stages = 12\n            total_stars = 3\n        elif gv <= 34:\n            total_subchapters = stream.read_int()\n            total_stages = 12\n            total_stars = 3\n        else:\n            total_subchapters = stream.read_int()\n            total_stages = stream.read_int()\n            total_stars = stream.read_int()\n        sub_chapters: list[SubChapterStars] = []\n        for _ in range(total_subchapters):\n            sub_chapters.append(\n                SubChapterStars.read(stream, total_stages, total_stars)\n            )\n        return TimedScoreChapters(sub_chapters)\n\n    def write(self, stream: core.Data, gv: core.GameVersion):\n        if gv < 20:\n            return\n        if gv <= 33:\n            pass\n        elif gv <= 34:\n            stream.write_int(len(self.sub_chapters))\n        else:\n            stream.write_int(len(self.sub_chapters))\n            try:\n                stream.write_int(\n                    len(self.sub_chapters[0].sub_chapters[0].stages)\n                )\n            except IndexError:\n                stream.write_int(0)\n            try:\n                stream.write_int(len(self.sub_chapters[0].sub_chapters))\n            except IndexError:\n                stream.write_int(0)\n        for sub_chapter in self.sub_chapters:\n            sub_chapter.write(stream)\n\n    def serialize(self) -> list[list[list[int]]]:\n        return [sub_chapter.serialize() for sub_chapter in self.sub_chapters]\n\n    @staticmethod\n    def deserialize(data: list[list[list[int]]]) -> TimedScoreChapters:\n        return TimedScoreChapters(\n            [SubChapterStars.deserialize(sub_chapter) for sub_chapter in data]\n        )\n\n    def __repr__(self) -> str:\n        return f\"Chapters(sub_chapters={self.sub_chapters})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n"
  },
  {
    "path": "src/bcsfe/core/game/map/tower.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass TowerChapters:\n    def __init__(self, chapters: core.Chapters):\n        self.chapters = chapters\n        self.item_obtain_states: list[list[bool]] = []\n\n    @staticmethod\n    def init() -> TowerChapters:\n        return TowerChapters(core.Chapters.init())\n\n    @staticmethod\n    def read(data: core.Data) -> TowerChapters:\n        ch = core.Chapters.read(data)\n        return TowerChapters(ch)\n\n    def write(self, data: core.Data):\n        self.chapters.write(data)\n\n    def read_item_obtain_states(self, data: core.Data):\n        total_stars = data.read_int()\n        total_stages = data.read_int()\n        self.item_obtain_states: list[list[bool]] = []\n        for _ in range(total_stars):\n            self.item_obtain_states.append(data.read_bool_list(total_stages))\n\n    def write_item_obtain_states(self, data: core.Data):\n        data.write_int(len(self.item_obtain_states))\n        try:\n            data.write_int(len(self.item_obtain_states[0]))\n        except IndexError:\n            data.write_int(0)\n        for item_obtain_state in self.item_obtain_states:\n            data.write_bool_list(item_obtain_state, write_length=False)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": self.chapters.serialize(),\n            \"item_obtain_states\": self.item_obtain_states,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> TowerChapters:\n        tower = TowerChapters(\n            core.Chapters.deserialize(data.get(\"chapters\", {})),\n        )\n        tower.item_obtain_states = data.get(\"item_obtain_states\", [])\n        return tower\n\n    def __repr__(self):\n        return f\"Tower({self.chapters}, {self.item_obtain_states})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def get_total_stars(self, chapter_id: int) -> int:\n        return len(self.chapters.chapters[chapter_id].chapters)\n\n    def get_total_stages(self, chapter_id: int, star: int) -> int:\n        return len(self.chapters.chapters[chapter_id].chapters[star].stages)\n\n    @staticmethod\n    def edit_towers(save_file: core.SaveFile):\n        towers = save_file.tower\n        towers.chapters.edit_chapters(save_file, \"V\", 7000)\n"
  },
  {
    "path": "src/bcsfe/core/game/map/uncanny.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\n\n\nclass UncannyChapters:\n    def __init__(self, chapters: core.Chapters, unknown: list[int]):\n        self.chapters = chapters\n        self.unknown = unknown\n\n    @staticmethod\n    def init() -> UncannyChapters:\n        return UncannyChapters(core.Chapters.init(), [])\n\n    @staticmethod\n    def read(data: core.Data) -> UncannyChapters:\n        ch = core.Chapters.read(data, read_every_time=False)\n        unknown = data.read_int_list(length=len(ch.chapters))\n        return UncannyChapters(ch, unknown)\n\n    def write(self, data: core.Data):\n        self.chapters.write(data, write_every_time=False)\n        data.write_int_list(self.unknown, write_length=False)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"chapters\": self.chapters.serialize(),\n            \"unknown\": self.unknown,\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> UncannyChapters:\n        return UncannyChapters(\n            core.Chapters.deserialize(data.get(\"chapters\", {})),\n            data.get(\"unknown\", []),\n        )\n\n    def __repr__(self):\n        return f\"Uncanny({self.chapters}, {self.unknown})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    @staticmethod\n    def edit_uncanny(save_file: core.SaveFile):\n        uncanny = save_file.uncanny\n        uncanny.chapters.edit_chapters(save_file, \"NA\", 13000)\n\n    @staticmethod\n    def edit_catamin_stages(save_file: core.SaveFile):\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            [\"change_clear_amount_catamin\", \"clear_unclear_stage_catamin\"],\n            dialog=\"catamin_stage_clear_q\",\n        ).single_choice()\n        if choice is None:\n            return None\n\n        if choice == 1:\n            names = core.MapNames(save_file, \"B\", base_index=14000)\n            map_ids = core.EventChapters.select_map_names(names.map_names)\n            if map_ids is None:\n                return None\n            if len(map_ids) >= 2:\n                choice2 = dialog_creator.ChoiceInput.from_reduced(\n                    [\"individual\", \"all_at_once\"], dialog=\"catamin_clear_amounts_q\"\n                ).single_choice()\n                if choice2 is None:\n                    return None\n            else:\n                choice2 = 1\n\n            if choice2 == 2:\n                clear_amount = dialog_creator.IntInput().get_input(\n                    \"enter_clear_amount_catamin\", {}\n                )[0]\n                if clear_amount is None:\n                    return None\n                for map_id in map_ids:\n                    save_file.event_stages.chapter_completion_count[14_000 + map_id] = (\n                        clear_amount\n                    )\n            elif choice == 1:\n                for map_id in map_ids:\n                    name = names.map_names.get(map_id) or core.localize(\"unknown_map\")\n                    clear_amount = dialog_creator.IntInput().get_input(\n                        \"enter_clear_amount_catamin_map\", {\"name\": name, \"id\": map_id}\n                    )[0]\n                    if clear_amount is None:\n                        return None\n                    save_file.event_stages.chapter_completion_count[14_000 + map_id] = (\n                        clear_amount\n                    )\n\n            color.ColoredText.localize(\"catamin_stage_success\")\n\n        elif choice == 2:\n            completed_chapters = save_file.catamin_stages.chapters.edit_chapters(\n                save_file, \"B\", 14000\n            )\n            if completed_chapters is None:\n                return None\n\n            # TODO: maybe in the future ask if the user wants to modify the chapter clear amounts\n"
  },
  {
    "path": "src/bcsfe/core/game/map/zero_legends.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import edits, color\n\n\nclass Stage:\n    def __init__(self, clear_times: int):\n        self.clear_times = clear_times\n\n    @staticmethod\n    def init() -> Stage:\n        return Stage(0)\n\n    @staticmethod\n    def read(data: core.Data) -> Stage:\n        clear_times = data.read_short()\n        return Stage(clear_times)\n\n    def write(self, data: core.Data):\n        data.write_short(self.clear_times)\n\n    def serialize(self) -> int:\n        return self.clear_times\n\n    @staticmethod\n    def deserialize(data: int) -> Stage:\n        return Stage(\n            data,\n        )\n\n    def __repr__(self):\n        return f\"Stage({self.clear_times})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def clear_stage(self, clear_amount: int = 1, ensure_cleared_only: bool = False):\n        if ensure_cleared_only:\n            self.clear_times = self.clear_times or clear_amount\n        else:\n            self.clear_times = clear_amount\n\n    def unclear_stage(self):\n        self.clear_times = 0\n\n\nclass Chapter:\n    def __init__(\n        self,\n        selected_stage: int,\n        clear_progress: int,\n        unlock_state: int,\n        stages: list[Stage],\n    ):\n        self.selected_stage = selected_stage\n        self.clear_progress = clear_progress\n        self.unlock_state = unlock_state\n        self.stages = stages\n\n        self.total_stages = 0\n\n    def clear_stage(\n        self,\n        index: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        if overwrite_clear_progress:\n            self.clear_progress = index + 1\n        else:\n            self.clear_progress = max(self.clear_progress, index + 1)\n        self.stages[index].clear_stage(clear_amount, ensure_cleared_only)\n        self.chapter_unlock_state = 3\n        if index == self.total_stages - 1:\n            return True\n        return False\n\n    def unclear_stage(self, index: int) -> bool:\n        self.clear_progress = min(self.clear_progress, index)\n        self.stages[index].unclear_stage()\n        return True\n\n    @staticmethod\n    def init() -> Chapter:\n        return Chapter(0, 0, 0, [])\n\n    @staticmethod\n    def read(data: core.Data) -> Chapter:\n        selected_stage = data.read_byte()\n        clear_progress = data.read_byte()\n        unlock_state = data.read_byte()\n        total_stages = data.read_short()\n        stages = [Stage.read(data) for _ in range(total_stages)]\n        return Chapter(\n            selected_stage,\n            clear_progress,\n            unlock_state,\n            stages,\n        )\n\n    def write(self, data: core.Data):\n        data.write_byte(self.selected_stage)\n        data.write_byte(self.clear_progress)\n        data.write_byte(self.unlock_state)\n        data.write_short(len(self.stages))\n        for stage in self.stages:\n            stage.write(data)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"selected_stage\": self.selected_stage,\n            \"clear_progress\": self.clear_progress,\n            \"unlock_state\": self.unlock_state,\n            \"stages\": [stage.serialize() for stage in self.stages],\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> Chapter:\n        return Chapter(\n            data.get(\"selected_stage\", 0),\n            data.get(\"clear_progress\", 0),\n            data.get(\"unlock_state\", 0),\n            [Stage.deserialize(stage) for stage in data.get(\"stages\", [])],\n        )\n\n    def __repr__(self):\n        return f\"Chapter({self.selected_stage}, {self.clear_progress}, {self.unlock_state}, {self.stages})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ChaptersStars:\n    def __init__(self, unknown: int, chapters: list[Chapter]):\n        self.unknown = unknown\n        self.chapters = chapters\n\n    def clear_stage(\n        self,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        finished = self.chapters[star].clear_stage(\n            stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished:\n            if star + 1 < len(self.chapters):\n                self.chapters[star + 1].chapter_unlock_state = 1\n        return finished\n\n    def unclear_stage(self, star: int, stage: int) -> bool:\n        finished = self.chapters[star].unclear_stage(stage)\n        if finished and star + 1 < len(self.chapters):\n            for chapter in self.chapters[star + 1 :]:\n                chapter.chapter_unlock_state = 0\n        return finished\n\n    @staticmethod\n    def init() -> ChaptersStars:\n        return ChaptersStars(0, [])\n\n    @staticmethod\n    def read(data: core.Data) -> ChaptersStars:\n        unknown = data.read_byte()\n        total_stars = data.read_byte()\n        chapters = [Chapter.read(data) for _ in range(total_stars)]\n        return ChaptersStars(\n            unknown,\n            chapters,\n        )\n\n    def write(self, data: core.Data):\n        data.write_byte(self.unknown)\n        data.write_byte(len(self.chapters))\n        for chapter in self.chapters:\n            chapter.write(data)\n\n    def serialize(self) -> dict[str, Any]:\n        return {\n            \"unknown\": self.unknown,\n            \"chapters\": [chapter.serialize() for chapter in self.chapters],\n        }\n\n    @staticmethod\n    def deserialize(data: dict[str, Any]) -> ChaptersStars:\n        return ChaptersStars(\n            data.get(\"unknown\", 0),\n            [Chapter.deserialize(chapter) for chapter in data.get(\"chapters\", [])],\n        )\n\n    def __repr__(self):\n        return f\"ChaptersStars({self.unknown}, {self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n\nclass ZeroLegendsChapters:\n    def __init__(self, chapters: list[ChaptersStars]):\n        self.chapters = chapters\n\n    def clear_stage(\n        self,\n        map: int,\n        star: int,\n        stage: int,\n        clear_amount: int = 1,\n        overwrite_clear_progress: bool = False,\n        ensure_cleared_only: bool = False,\n    ) -> bool:\n        self.create(map)\n        finished = self.chapters[map].clear_stage(\n            star, stage, clear_amount, overwrite_clear_progress, ensure_cleared_only\n        )\n        if finished and map + 1 < len(self.chapters):\n            self.chapters[map + 1].chapters[0].chapter_unlock_state = 1\n\n        return finished\n\n    def unclear_stage(self, map: int, star: int, stage: int) -> bool:\n        self.create(map)\n        finished = self.chapters[map].unclear_stage(star, stage)\n        if finished and map + 1 < len(self.chapters) and star == 0:\n            for chapter in self.chapters[map + 1].chapters:\n                chapter.chapter_unlock_state = 0\n\n        return finished\n\n    @staticmethod\n    def init() -> ZeroLegendsChapters:\n        return ZeroLegendsChapters([])\n\n    @staticmethod\n    def read(data: core.Data) -> ZeroLegendsChapters:\n        total_chapters = data.read_short()\n        chapters = [ChaptersStars.read(data) for _ in range(total_chapters)]\n        return ZeroLegendsChapters(\n            chapters,\n        )\n\n    def write(self, data: core.Data):\n        data.write_short(len(self.chapters))\n        for chapter in self.chapters:\n            chapter.write(data)\n\n    def serialize(self) -> list[dict[str, Any]]:\n        return [chapter.serialize() for chapter in self.chapters]\n\n    @staticmethod\n    def deserialize(data: list[dict[str, Any]]) -> ZeroLegendsChapters:\n        return ZeroLegendsChapters(\n            [ChaptersStars.deserialize(chapter) for chapter in data],\n        )\n\n    def __repr__(self):\n        return f\"Chapters({self.chapters})\"\n\n    def __str__(self):\n        return self.__repr__()\n\n    def get_total_stars(self, chapter_id: int) -> int:\n        return len(self.chapters[chapter_id].chapters)\n\n    def get_total_stages(self, chapter_id: int, star: int) -> int:\n        return len(self.chapters[chapter_id].chapters[star].stages)\n\n    def create(self, chapter_id: int):\n        diff = chapter_id - len(self.chapters)\n\n        if diff >= 0:\n            for _ in range(diff + 1):\n                stages = [Stage(0)] * self.get_total_stages(0, 0)\n                chapters = [Chapter(0, 0, 0, stages)] * self.get_total_stars(0)\n                chapters_stars = ChaptersStars(0, chapters)\n                self.chapters.append(chapters_stars)\n\n    @staticmethod\n    def edit_zero_legends(save_file: core.SaveFile):\n        color.ColoredText.localize(\"zero_legends_warning\")\n        zero_legends_chapters = save_file.zero_legends\n        zero_legends_chapters.edit_chapters(save_file, \"ND\", base_index=34000)\n\n    @staticmethod\n    def edit_catclaw_championships(save_file: core.SaveFile):\n        zero_legends_chapters = save_file.dojo_chapters\n        zero_legends_chapters.edit_chapters(save_file, \"G\", 37000, True)\n\n    def edit_chapters(\n        self,\n        save_file: core.SaveFile,\n        letter_code: str,\n        base_index: int,\n        no_r_prefix: bool = False,\n    ):\n        edits.map.edit_chapters(\n            save_file, self, letter_code, no_r_prefix=no_r_prefix, base_index=base_index\n        )\n\n    def unclear_rest(self, stages: list[int], stars: int, id: int):\n        if not stages:\n            return\n        for star in range(stars, self.get_total_stars(id)):\n            for stage in range(max(stages), self.get_total_stages(id, star)):\n                self.chapters[id].chapters[star].stages[stage].clear_times = 0\n                self.chapters[id].chapters[star].clear_progress = 0\n\n    def set_total_stages(self, map: int, total_stages: int):\n        self.create(map)\n        for chapter in self.chapters[map].chapters:\n            chapter.total_stages = total_stages\n"
  },
  {
    "path": "src/bcsfe/core/game_version.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass GameVersion:\n    \"\"\"A class to represent a game version.\"\"\"\n\n    def __init__(self, game_version: int):\n        \"\"\"Initializes a new instance of the GameVersion class.\n\n        Args:\n            game_version (int): Game version as an integer. e.g 120102 for 12.1.2\n        \"\"\"\n        self.game_version = game_version\n\n    def to_string(self) -> str:\n        \"\"\"Converts the game version to a string.\n\n        Returns:\n            str: Game version as a string. e.g 12.1.2\n        \"\"\"\n        split_gv = str(self.game_version).zfill(6)\n        split_gv = [\n            str(int(split_gv[i : i + 2])) for i in range(0, len(split_gv), 2)\n        ]\n        return \".\".join(split_gv)\n\n    def get_parts_zfill(self) -> list[str]:\n        \"\"\"Gets the parts of the game version as a list of strings with leading zeros.\n\n        Returns:\n            list[str]: Game version parts as strings with leading zeros. e.g [\"12\", \"01\", \"02\"]\n        \"\"\"\n        return [part.zfill(2) for part in self.to_string().split(\".\")]\n\n    def get_parts(self) -> list[int]:\n        \"\"\"Gets the parts of the game version as a list of integers.\n\n        Returns:\n            list[int]: Game version parts as integers. e.g [12, 1, 2]\n        \"\"\"\n        return [int(part) for part in self.get_parts_zfill()]\n\n    def format(self) -> str:\n        \"\"\"Formats the game version as a string with leading zeros.\n\n        Returns:\n            str: Game version as a string with leading zeros. e.g 12.01.02\n        \"\"\"\n        parts = self.get_parts_zfill()\n        string = \"\"\n        for part in parts:\n            string += f\"{part}.\"\n        return f\"{string[:-1]}\"\n\n    def __str__(self) -> str:\n        \"\"\"Converts the game version to a string.\n\n        Returns:\n            str: Game version as a string. e.g 12.1.2\n        \"\"\"\n        return self.to_string()\n\n    def __repr__(self) -> str:\n        \"\"\"Converts the game version object to a string.\n\n        Returns:\n            str: Game version object as a string. e.g game_version(120102) 12.1.2\n        \"\"\"\n        return f\"game_version({self.game_version}) {self.to_string()}\"\n\n    @staticmethod\n    def read(data: core.Data) -> GameVersion:\n        \"\"\"Reads a 4 byte int from a Data object.\n\n        Args:\n            data (core.Data): Data object to read from.\n\n        Returns:\n            GameVersion: Game version read from the Data object.\n        \"\"\"\n        return GameVersion(data.read_int())\n\n    def write(self, data: core.Data):\n        \"\"\"Writes the 4 byte game version to a Data object.\n\n        Args:\n            data (core.Data): Data object to write to.\n        \"\"\"\n        data.write_int(self.game_version)\n\n    def serialize(self) -> dict[str, Any]:\n        \"\"\"Serializes the game version to a dictionary.\n\n        Returns:\n            dict[str, Any]: Serialized game version.\n        \"\"\"\n        return {\"game_version\": self.game_version}\n\n    @staticmethod\n    def deserialize(game_version: dict[str, Any]) -> GameVersion:\n        \"\"\"Deserializes a game version from a dictionary.\n\n        Args:\n            game_version (dict[str, Any]): Serialized game version.\n\n        Returns:\n            GameVersion: Deserialized game version.\n        \"\"\"\n        return GameVersion(game_version[\"game_version\"])\n\n    @staticmethod\n    def from_string(game_version: str) -> GameVersion:\n        \"\"\"Converts a string to a GameVersion object.\n\n        Args:\n            game_version (str): Game version as a string. e.g 12.1.2\n\n        Returns:\n            GameVersion: Game version as a GameVersion object.\n        \"\"\"\n        split_gv = game_version.split(\".\")\n        if len(split_gv) == 2:\n            split_gv.append(\"0\")\n        final = \"\"\n        for split in split_gv:\n            final += split.zfill(2)\n        return GameVersion(int(final))\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Checks if the game version is equal to another object.\n\n        Args:\n            other (Any): Object to compare to.\n\n        Returns:\n            bool: True if the game version is equal to the other object, False otherwise.\n        \"\"\"\n        if isinstance(other, GameVersion):\n            return self.game_version == other.game_version\n        elif isinstance(other, int):\n            return self.game_version == other\n        elif isinstance(other, str):\n            return (\n                self.game_version == GameVersion.from_string(other).game_version\n            )\n        else:\n            return False\n\n    def __ne__(self, other: Any) -> bool:\n        \"\"\"Checks if the game version is not equal to another object.\n\n        Args:\n            other (Any): Object to compare to.\n\n        Returns:\n            bool: True if the game version is not equal to the other object, False otherwise.\n        \"\"\"\n        return not self.__eq__(other)\n\n    def __lt__(self, other: Any) -> bool:\n        \"\"\"Checks if the game version is less than another object.\n\n        Args:\n            other (Any): Object to compare to.\n\n        Returns:\n            bool: True if the game version is less than the other object, False otherwise.\n        \"\"\"\n        if isinstance(other, GameVersion):\n            return self.game_version < other.game_version\n        elif isinstance(other, int):\n            return self.game_version < other\n        elif isinstance(other, str):\n            return (\n                self.game_version < GameVersion.from_string(other).game_version\n            )\n        else:\n            return False\n\n    def __le__(self, other: Any) -> bool:\n        \"\"\"Checks if the game version is less than or equal to another object.\n\n        Args:\n            other (Any): Object to compare to.\n\n        Returns:\n            bool: True if the game version is less than or equal to the other object, False otherwise.\n        \"\"\"\n        return self.__lt__(other) or self.__eq__(other)\n\n    def __gt__(self, other: Any) -> bool:\n        \"\"\"Checks if the game version is greater than another object.\n\n        Args:\n            other (Any): Object to compare to.\n\n        Returns:\n            bool: True if the game version is greater than the other object, False otherwise.\n        \"\"\"\n        return not self.__le__(other)\n\n    def __ge__(self, other: Any) -> bool:\n        \"\"\"Checks if the game version is greater than or equal to another object.\n\n        Args:\n            other (Any): Object to compare to.\n\n        Returns:\n            bool: True if the game version is greater than or equal to the other object, False otherwise.\n        \"\"\"\n        return not self.__lt__(other)\n\n    def __add__(self, other: Any) -> GameVersion:\n        \"\"\"Adds the game version to another object.\n\n        Args:\n            other (Any): Object to add to.\n\n        Returns:\n            GameVersion: Game version added to the other object.\n        \"\"\"\n        if isinstance(other, GameVersion):\n            return GameVersion(self.game_version + other.game_version)\n        elif isinstance(other, int):\n            return GameVersion(self.game_version + other)\n        elif isinstance(other, str):\n            return GameVersion(\n                self.game_version + GameVersion.from_string(other).game_version\n            )\n        else:\n            return NotImplemented\n\n    def __sub__(self, other: Any) -> GameVersion:\n        \"\"\"Subtracts the game version from another object.\n\n        Args:\n            other (Any): Object to subtract from.\n\n        Returns:\n            GameVersion: Game version subtracted from the other object.\n        \"\"\"\n        return self.__add__(-other)\n"
  },
  {
    "path": "src/bcsfe/core/io/__init__.py",
    "content": "from bcsfe.core.io import (\n    bc_csv,\n    path,\n    data,\n    command,\n    yaml,\n    config,\n    json_file,\n    save,\n    thread_helper,\n    root_handler,\n    adb_handler,\n    git_handler,\n    waydroid,\n)\n\n__all__ = [\n    \"bc_csv\",\n    \"path\",\n    \"data\",\n    \"command\",\n    \"yaml\",\n    \"config\",\n    \"json_file\",\n    \"save\",\n    \"thread_helper\",\n    \"root_handler\",\n    \"adb_handler\",\n    \"git_handler\",\n    \"waydroid\",\n]\n"
  },
  {
    "path": "src/bcsfe/core/io/adb_handler.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.core import io\nfrom bcsfe.cli import dialog_creator, color\n\n\nclass DeviceIDNotSet(Exception):\n    pass\n\n\nclass AdbNotInstalled(Exception):\n    def __init__(self, result: core.CommandResult):\n        self.result = result\n\n\nclass AdbHandler(io.root_handler.RootHandler):\n    def __init__(self, root: bool = True):\n        self.root_avail = root\n        adb_path = core.Path(core.core_data.config.get_str(core.ConfigKey.ADB_PATH))\n        self.check_adb_installed(adb_path)\n        self.adb_path = adb_path\n        self.start_server()\n        self.device_id = None\n        self.package_name = None\n        self.root_result = None\n\n    @staticmethod\n    def display_no_adb_error(e: AdbNotInstalled):\n        color.ColoredText.localize(\n            \"adb_not_installed\",\n            path=core.core_data.config.get_str(core.ConfigKey.ADB_PATH),\n            error=e,\n        )\n\n    def check_adb_installed(self, path: core.Path):\n        result = path.run(\"version\")\n        if not result.success:\n            raise AdbNotInstalled(result)\n\n    def adb_root_success(self) -> bool:\n        if self.root_result is None:\n            return False\n        result = self.root_result.result.strip()\n        return (\n            result != \"adbd cannot run as root in production builds\"\n            and result != \"not available in Waydroid\"\n        )\n\n    def start_server(self) -> core.CommandResult:\n        return self.adb_path.run(\"start-server\")\n\n    def kill_server(self) -> core.CommandResult:\n        return self.adb_path.run(\"kill-server\")\n\n    def root(self) -> core.CommandResult:\n        return self.adb_path.run(f\"-s {self.get_device()} root\")\n\n    def get_connected_devices(self) -> list[str]:\n        devices = self.adb_path.run(\"devices\").result.split(\"\\n\")\n        devices = [device.split(\"\\t\")[0] for device in devices[1:-2]]\n        return devices\n\n    def set_device(self, device_id: str):\n        self.device_id = device_id\n        if self.root_avail:\n            self.root_result = self.root()\n\n    def get_device(self) -> str:\n        if self.device_id is None:\n            raise DeviceIDNotSet(\"Device ID is not set\")\n        return self.device_id\n\n    def get_device_name(self) -> str:\n        return self.run_shell(\"getprop ro.product.model\").result.strip()\n\n    def run_shell(self, command: str) -> core.CommandResult:\n        return self.adb_path.run(f'-s {self.get_device()} shell \"{command}\"')\n\n    def run_root_shell(self, command: str) -> core.CommandResult:\n        return self.run_shell(f\"su -c '{command}'\")\n\n    def adb_pull_file(\n        self, device_path: core.Path, local_path: core.Path\n    ) -> core.CommandResult:\n        return self.adb_path.run(\n            f'-s {self.get_device()} pull \"{device_path.to_str_forwards()}\" \"{local_path}\"',\n        )\n\n    def pull_file(\n        self, device_path: core.Path, local_path: core.Path\n    ) -> core.CommandResult:\n        if not self.adb_root_success():\n            result = self.run_root_shell(\n                f\"cp {device_path.to_str_forwards()} /sdcard/{device_path.basename()} && chmod o+rw /sdcard/{device_path.basename()}\"\n            )\n            if result.exit_code != 0:\n                return result\n            device_path = core.Path(\"/sdcard/\").add(device_path.basename())\n\n        result = self.adb_pull_file(device_path, local_path)\n\n        if not result.success:\n            return result\n        if not self.adb_root_success():\n            result2 = self.run_shell(f\"rm /sdcard/{device_path.basename()}\")\n            if result2.exit_code != 0:\n                return result2\n        return result\n\n    def adb_push_file(\n        self, local_path: core.Path, device_path: core.Path\n    ) -> core.CommandResult:\n        return self.adb_path.run(\n            f'-s {self.get_device()} push \"{local_path}\" \"{device_path.to_str_forwards()}\"'\n        )\n\n    def push_file(\n        self, local_path: core.Path, device_path: core.Path\n    ) -> core.CommandResult:\n        orignal_device_path = device_path.copy_object()\n        if not self.adb_root_success():\n            device_path = core.Path(\"/sdcard/\").add(device_path.basename())\n\n        result = self.adb_push_file(local_path, device_path)\n        if not result.success:\n            return result\n        if not self.adb_root_success():\n            result2 = self.run_root_shell(\n                f\"cp '/sdcard/{device_path.basename()}' '{orignal_device_path.to_str_forwards()}' && chmod o+rw '{orignal_device_path.to_str_forwards()}'\"\n            )\n            result3 = self.run_shell(f\"rm '/sdcard/{device_path.basename()}'\")\n            if result2.exit_code != 0:\n                return result2\n            if result3.exit_code != 0:\n                return result3\n\n        return result\n\n    def stat_file(self, device_path: core.Path) -> core.CommandResult:\n        return self.run_shell(f\"stat {device_path.to_str_forwards()}\")\n\n    def does_file_exist(self, device_path: core.Path) -> bool:\n        return self.stat_file(device_path).success\n\n    def get_battlecats_packages(self) -> list[str]:\n        cmd = \"find /data/data/ -name SAVE_DATA -mindepth 3 -maxdepth 3\"\n        result = self.run_root_shell(cmd)\n        if not result.success:\n            return []\n        packages: list[str] = []\n        for package in result.result.split(\"\\n\"):\n            parts = package.split(\"/\")\n            if len(parts) < 4:\n                continue\n            packages.append(package.split(\"/\")[3])\n        return packages\n\n    def save_battlecats_save(self, local_path: core.Path) -> core.CommandResult:\n        result = self.pull_file(self.get_battlecats_save_path(), local_path)\n        return result\n\n    def load_battlecats_save(self, local_path: core.Path) -> core.CommandResult:\n        return self.push_file(local_path, self.get_battlecats_save_path())\n\n    def close_game(self) -> core.CommandResult:\n        return self.run_shell(f\"am force-stop {self.get_package_name()}\")\n\n    def run_game(self) -> core.CommandResult:\n        return self.run_shell(f\"monkey --pct-syskeys 0 -p {self.get_package_name()} 1\")\n\n    def select_device(self) -> bool:\n        devices = self.get_connected_devices()\n        device = dialog_creator.ChoiceInput.from_reduced(\n            devices, dialog=\"select_device\", single_choice=True\n        ).single_choice()\n        if not device:\n            color.ColoredText.localize(\"no_device_error\")\n            return False\n\n        self.set_device(devices[device - 1])\n        return True\n"
  },
  {
    "path": "src/bcsfe/core/io/bc_csv.py",
    "content": "from __future__ import annotations\nimport csv as csv_module\nimport enum\nfrom typing import Any\nimport typing\nfrom bcsfe import core\n\n\nclass DelimeterType(enum.Enum):\n    COMMA = \",\"\n    TAB = \"\\t\"\n    PIPE = \"|\"\n\n\nclass Delimeter:\n    def __init__(self, de: DelimeterType | str):\n        if isinstance(de, str):\n            self.delimiter = DelimeterType(de)\n        else:\n            self.delimiter = de\n\n    @staticmethod\n    def from_country_code_res(cc: core.CountryCode) -> Delimeter:\n        if cc.get_cc_lang() == core.CountryCodeType.JP:\n            return Delimeter(DelimeterType.COMMA)\n        return Delimeter(DelimeterType.PIPE)\n\n    def __str__(self) -> str:\n        return self.delimiter.value\n\n\nclass Cell:\n    def __init__(self, dt: core.Data):\n        self.data = dt\n\n    def to_str(self) -> str:\n        return self.data.to_str()\n\n    def to_int(self) -> int:\n        return self.data.to_int()\n\n    def to_bool(self) -> bool:\n        return self.data.to_bool()\n\n    def __repr__(self) -> str:\n        return f\"Cell({self.data})\"\n\n    def __str__(self) -> str:\n        return self.data.to_str()\n\n\nclass Row:\n    def __init__(self, cells: list[Cell]):\n        self.cells = cells\n        self.index = 0\n\n    @typing.overload\n    def __getitem__(self, index: int) -> Cell: ...\n\n    @typing.overload\n    def __getitem__(self, index: slice) -> Row: ...\n\n    def __getitem__(self, index: int | slice) -> Cell | Row:\n        if isinstance(index, int):\n            try:\n                return self.cells[index]\n            except IndexError:\n                return Cell(core.Data(\"\"))\n        try:\n            return Row(self.cells[index])\n        except IndexError:\n            return Row([])\n\n    def __len__(self) -> int:\n        return len(self.cells)\n\n    @staticmethod\n    def from_list(dt: list[core.Data]) -> Row:\n        cells: list[Cell] = []\n        for item in dt:\n            cells.append(Cell(item))\n        return Row(cells)\n\n    def __repr__(self) -> str:\n        return f\"Row({self.cells})\"\n\n    def __str__(self) -> str:\n        return self.__repr__()\n\n    def __iter__(self):\n        self.index = 0\n        return iter(self.cells)\n\n    def __next__(self):\n        if self.index >= len(self.cells):\n            raise StopIteration\n        else:\n            self.index += 1\n            return self.cells[self.index - 1]\n\n    def next(self):\n        return next(self)\n\n    def next_opt(self) -> Cell | None:\n        if self.done():\n            return None\n        return self.next()\n\n    def done(self):\n        return self.index >= len(self.cells)\n\n    def next_int(self) -> int:\n        return self.next().to_int()\n\n    def next_str(self) -> str:\n        return self.next().to_str()\n\n    def next_bool(self) -> bool:\n        return self.next().to_bool()\n\n    def next_int_opt(self) -> int | None:\n        val = self.next_opt()\n        if val is None:\n            return None\n        return val.to_int()\n\n    def next_str_opt(self) -> str | None:\n        val = self.next_opt()\n        if val is None:\n            return None\n        return val.to_str()\n\n    def next_bool_opt(self) -> bool | None:\n        val = self.next_opt()\n        if val is None:\n            return None\n        return val.to_bool()\n\n    def to_str_list(self) -> list[str]:\n        return [cell.to_str() for cell in self.cells]\n\n    def to_int_list(self) -> list[int]:\n        return [cell.to_int() for cell in self.cells]\n\n\nclass CSV:\n    def __init__(\n        self,\n        file_data: core.Data,\n        delimiter: Delimeter | str = Delimeter(DelimeterType.COMMA),\n        remove_padding: bool = False,\n        remove_comments: bool = True,\n        remove_empty: bool = True,\n    ):\n        self.file_data = file_data\n        if remove_padding:\n            data = self.file_data.unpad_pkcs7()\n            if data is None:\n                self.file_data = self.file_data\n            else:\n                self.file_data = data\n\n        self.delimiter = delimiter\n        self.remove_comments = remove_comments\n        self.remove_empty = remove_empty\n        self.index = 0\n        self.parse()\n\n    def parse(self):\n        reader = csv_module.reader(\n            self.file_data.data.decode(\"utf-8\").splitlines(),\n            delimiter=str(self.delimiter),\n        )\n        self.lines: list[Row] = []\n        for row in reader:\n            new_row: list[core.Data] = []\n            full_row = f\"{str(self.delimiter)}\".join(row)\n            if self.remove_comments:\n                full_row = full_row.split(\"//\")[0]\n            row = full_row.split(str(self.delimiter))\n            if row or not self.remove_empty:\n                for item in row:\n                    item = item.strip()\n                    if item or not self.remove_empty:\n                        new_row.append(core.Data(item))\n                if new_row or not self.remove_empty:\n                    self.lines.append(Row.from_list(new_row))\n\n    def get_row(self, index: int) -> Row:\n        try:\n            return self.lines[index]\n        except IndexError:\n            return Row([])\n\n    def __getitem__(self, index: int) -> Row:\n        return self.get_row(index)\n\n    def __len__(self) -> int:\n        return len(self.lines)\n\n    @staticmethod\n    def from_file(\n        pt: core.Path, delimiter: Delimeter = Delimeter(DelimeterType.COMMA)\n    ) -> CSV:\n        return CSV(pt.read(), delimiter)\n\n    def add_line(self, line: list[Any] | Any):\n        if not isinstance(line, list):\n            line = [line]\n        new_line: list[core.Data] = []\n        for item in line:\n            new_line.append(core.Data(str(item)))\n        self.lines.append(Row.from_list(new_line))\n\n    def set_line(self, index: int, line: list[Any]):\n        new_line: list[core.Data] = []\n        for item in line:\n            new_line.append(core.Data(item))\n        try:\n            self.lines[index] = Row.from_list(new_line)\n        except IndexError:\n            self.lines.append(Row.from_list(new_line))\n\n    def to_data(self) -> core.Data:\n        csv: list[str] = []\n        for line in self.lines:\n            for i, item in enumerate(line):\n                csv.append(str(item))\n                if i != len(line) - 1:\n                    csv.append(str(self.delimiter))\n            csv.append(\"\\r\\n\")\n        return core.Data(\"\".join(csv))\n\n    def read_line(self) -> Row | None:\n        try:\n            line = self.lines[self.index]\n        except IndexError:\n            return None\n\n        self.index += 1\n        return line\n\n    def reset_index(self):\n        self.index = 0\n\n    def has_line(self) -> bool:\n        return self.index < len(self.lines)\n\n    def __iter__(self):\n        return self\n\n    def __next__(self) -> Row:\n        line = self.read_line()\n        if line is None:\n            raise StopIteration\n        return line\n\n    def extend(self, length: int, sub_length: int = 0):\n        for _ in range(length):\n            if sub_length == 0:\n                self.lines.append(Row.from_list([]))\n            else:\n                self.lines.append(Row.from_list([core.Data(\"\")] * sub_length))\n"
  },
  {
    "path": "src/bcsfe/core/io/command.py",
    "content": "from __future__ import annotations\nimport subprocess\nimport threading\n\n\nclass CommandResult:\n    def __init__(self, result: str, exit_code: int):\n        self.result = result\n        self.exit_code = exit_code\n\n    def __str__(self) -> str:\n        return self.result\n\n    def __repr__(self) -> str:\n        return f\"Result({self.result!r}, {self.exit_code!r})\"\n\n    @property\n    def success(self) -> bool:\n        return self.exit_code == 0\n\n    @staticmethod\n    def create_success(result: str = \"\") -> CommandResult:\n        return CommandResult(result, 0)\n\n    @staticmethod\n    def create_failure(result: str = \"\") -> CommandResult:\n        return CommandResult(result, 1)\n\n\nclass Command:\n    def __init__(self, command: str, display_output: bool = True):\n        self.command = command\n        self.display_output = display_output\n\n    def run(self, inputData: str = \"\\n\") -> CommandResult:\n        self.process = subprocess.Popen(\n            self.command,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.STDOUT,\n            stdin=subprocess.PIPE,\n            shell=True,\n            universal_newlines=True,\n        )\n        output, _ = self.process.communicate(inputData)\n        return_code = self.process.wait()\n        return CommandResult(output, return_code)\n\n    def run_in_thread(self, inputData: str = \"\\n\") -> None:\n        self.thread = threading.Thread(target=self.run, args=(inputData,))\n        self.thread.start()\n"
  },
  {
    "path": "src/bcsfe/core/io/config.py",
    "content": "from __future__ import annotations\nimport enum\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color, dialog_creator\nimport requests\n\n\nclass ConfigKey(enum.Enum):\n    UPDATE_TO_BETA = \"update_to_beta\"\n    SHOW_UPDATE_MESSAGE = \"show_update_message\"\n    LOCALE = \"locale\"\n    SHOW_MISSING_LOCALE_KEYS = \"show_missing_locale_keys\"\n    DISABLE_MAXES = \"disable_maxes\"\n    MAX_BACKUPS = \"max_backups\"\n    THEME = \"theme\"\n    RESET_CAT_DATA = \"reset_cat_data\"\n    SET_CAT_CURRENT_FORMS = \"set_cat_current_forms\"\n    STRICT_UPGRADE = \"strict_upgrade\"\n    SEPARATE_CAT_EDIT_OPTIONS = \"separate_cat_edit_options\"\n    STRICT_BAN_PREVENTION = \"strict_ban_prevention\"\n    MAX_REQUEST_TIMEOUT = \"max_request_timeout\"\n    GAME_DATA_REPO = \"game_data_repo\"\n    FORCE_LANG_GAME_DATA = \"force_lang_game_data\"\n    CLEAR_TUTORIAL_ON_LOAD = \"clear_tutorial_on_load\"\n    REMOVE_BAN_MESSAGE_ON_LOAD = \"remove_ban_message_on_load\"\n    UNLOCK_CAT_ON_EDIT = \"unlock_cat_on_edit\"\n    USE_FILE_DIALOG = \"use_file_dialog\"\n    ADB_PATH = \"adb_path\"\n    IGNORE_PARSE_ERROR = \"ignore_parse_error\"\n    USE_WAYDROID = \"use_waydroid\"\n    USE_PKEXEC_WAYDROID = \"use_pkexec_waydroid\"\n\n\nclass Config:\n    def __init__(self, path: core.Path | None, print_yaml_err: bool = True):\n        if path is None:\n            path = Config.get_config_path()\n        config = core.YamlFile(path, print_yaml_err)\n        self.config: dict[ConfigKey, Any] = {}\n        for key, value in config.yaml.items():\n            try:\n                self.config[ConfigKey(key)] = value\n            except ValueError:\n                pass\n        self.config_object = config\n        self.initialize_config()\n\n    @staticmethod\n    def get_config_path() -> core.Path:\n        return core.Path.get_config_folder().add(\"config.yaml\")\n\n    def __getitem__(self, key: ConfigKey) -> Any:\n        return self.config[key]\n\n    def __setitem__(self, key: ConfigKey, value: Any) -> None:\n        self.config[key] = value\n\n    def __contains__(self, key: ConfigKey) -> bool:\n        return key in self.config\n\n    @staticmethod\n    def get_defaults() -> dict[ConfigKey, Any]:\n        initial_values = {\n            ConfigKey.UPDATE_TO_BETA: False,\n            ConfigKey.SHOW_UPDATE_MESSAGE: True,\n            ConfigKey.LOCALE: \"en\",\n            ConfigKey.SHOW_MISSING_LOCALE_KEYS: False,\n            ConfigKey.DISABLE_MAXES: False,\n            ConfigKey.MAX_BACKUPS: 50,\n            ConfigKey.THEME: \"default\",\n            ConfigKey.RESET_CAT_DATA: True,\n            ConfigKey.SET_CAT_CURRENT_FORMS: True,\n            ConfigKey.STRICT_UPGRADE: False,\n            ConfigKey.SEPARATE_CAT_EDIT_OPTIONS: True,\n            ConfigKey.STRICT_BAN_PREVENTION: False,\n            ConfigKey.MAX_REQUEST_TIMEOUT: 30,\n            ConfigKey.GAME_DATA_REPO: \"https://git.battlecatsmodding.org/fieryhenry/BCData/raw/branch/main/metadata.json\",\n            ConfigKey.FORCE_LANG_GAME_DATA: False,\n            ConfigKey.CLEAR_TUTORIAL_ON_LOAD: True,\n            ConfigKey.REMOVE_BAN_MESSAGE_ON_LOAD: True,\n            ConfigKey.UNLOCK_CAT_ON_EDIT: True,\n            ConfigKey.USE_FILE_DIALOG: True,\n            ConfigKey.ADB_PATH: \"adb\",\n            ConfigKey.IGNORE_PARSE_ERROR: False,\n            ConfigKey.USE_WAYDROID: False,\n            ConfigKey.USE_PKEXEC_WAYDROID: True,\n        }\n        return initial_values\n\n    def get_default(self, key: ConfigKey) -> Any:\n        value = Config.get_defaults()[key]\n        return value\n\n    def set_default(self, key: ConfigKey):\n        value = self.get_default(key)\n        self.config[key] = value\n        self.save()\n        return value\n\n    def initialize_config(self):\n        initial_values = Config.get_defaults()\n\n        for key, value in initial_values.items():\n            if key not in self.config:\n                self.config[key] = value\n        self.save()\n\n    def save(self):\n        for key, value in self.config.items():\n            self.config_object.yaml[key.value] = value\n        self.config_object.save()\n\n    def get(self, key: ConfigKey) -> Any:\n        value = self.config[key]\n        if value is None:\n            return self.set_default(key)\n        return value\n\n    def get_game_data_repo(self, fix_old_repo: bool = True) -> str:\n        if fix_old_repo and self.get_str(ConfigKey.GAME_DATA_REPO) in [\n            \"https://raw.githubusercontent.com/fieryhenry/BCData/master/\",\n            \"https://git.fyhenry.uk/henry/BCData/raw/branch/main/info.json\",\n        ]:\n            self.set(\n                ConfigKey.GAME_DATA_REPO, self.get_default(ConfigKey.GAME_DATA_REPO)\n            )\n        return self.get_str(ConfigKey.GAME_DATA_REPO)\n\n    def get_str(self, key: ConfigKey) -> str:\n        value = self.get(key)\n        if not isinstance(value, str):\n            return self.set_default(key)\n        return value\n\n    def get_bool(self, key: ConfigKey) -> bool:\n        value = self.get(key)\n        if not isinstance(value, bool):\n            return self.set_default(key)\n        return value\n\n    def get_int(self, key: ConfigKey) -> int:\n        value = self.get(key)\n        if not isinstance(value, int):\n            return self.set_default(key)\n        return value\n\n    def reset(self):\n        self.config.clear()\n        self.config_object.remove()\n        self.initialize_config()\n\n    def set(self, key: ConfigKey, value: Any):\n        self.config[key] = value\n        self.save()\n\n    def get_bool_text(self, value: bool) -> str:\n        if value:\n            return core.core_data.local_manager.get_key(\"enabled\")\n        return core.core_data.local_manager.get_key(\"disabled\")\n\n    def get_full_input_localized(\n        self, key: ConfigKey, current_value: str, default_value: str\n    ) -> str:\n        return core.core_data.local_manager.get_key(\n            \"config_full\",\n            key_desc=core.core_data.local_manager.get_key(\n                Config.get_desc_key(key),\n                current_value=current_value,\n                default_value=default_value,\n                escape=False,\n            ),\n            escape=False,\n        )\n\n    def edit_bool(self, key: ConfigKey):\n        value = self.get_bool(key)\n        color.ColoredText(\n            self.get_full_input_localized(\n                key,\n                self.get_bool_text(value),\n                self.get_bool_text(self.get_default(key)),\n            ),\n        )\n        choice = dialog_creator.ChoiceInput(\n            [\"enable\", \"disable\"],\n            [\"enable\", \"disable\"],\n            [],\n            {},\n            \"enable_disable_dialog\",\n            True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        if choice == 0:\n            value = True\n        elif choice == 1:\n            value = False\n        self.set(key, value)\n        print()\n        color.ColoredText.localize(\n            \"config_success\",\n        )\n\n    @staticmethod\n    def get_desc_key(key: ConfigKey) -> str:\n        return key.value + \"_desc\"\n\n    def edit_int(self, key: ConfigKey):\n        text = self.get_full_input_localized(\n            key, str(self.get_int(key)), str(self.get_default(key))\n        )\n        color.ColoredText.localize(text)\n        value = dialog_creator.SingleEditor(\n            key.value, self.get_int(key), signed=False, localized_item=True\n        ).edit()\n        self.set(key, value)\n\n        color.ColoredText.localize(\n            \"config_success\",\n        )\n\n    def edit_game_data_repo(self):\n        text = self.get_full_input_localized(\n            ConfigKey.GAME_DATA_REPO,\n            self.get_str(ConfigKey.GAME_DATA_REPO),\n            self.get_default(ConfigKey.GAME_DATA_REPO),\n        )\n        color.ColoredText.localize(text)\n        value = dialog_creator.StringInput().get_input_locale(\n            \"game_data_repo_dialog\", {}\n        )\n        if value is None:\n            value = self.get_default(ConfigKey.GAME_DATA_REPO)\n        color.ColoredText.localize(\"validating_game_repo\")\n        try:\n            resp = core.RequestHandler(value).get()\n        except (requests.exceptions.MissingSchema, requests.exceptions.InvalidSchema):\n            color.ColoredText.localize(\"invalid_url\")\n            return\n        if resp is None:\n            color.ColoredText.localize(\"no_internet_or_connection_error\")\n            return\n        if resp.status_code != 200:\n            color.ColoredText.localize(\n                \"invalid_response\", response_code=resp.status_code\n            )\n            return\n        self.set(ConfigKey.GAME_DATA_REPO, value)\n        color.ColoredText.localize(\n            \"config_success\",\n        )\n\n    def edit_str(self, key: ConfigKey):\n        text = self.get_full_input_localized(\n            key,\n            self.get_str(key),\n            self.get_default(key),\n        )\n\n        color.ColoredText.localize(text)\n\n        str_val = core.core_data.local_manager.get_key(key.value)\n\n        value = dialog_creator.StringInput().get_input_locale(\n            \"string_config_dialog\", {\"val\": str_val}\n        )\n        if value is None:\n            return\n\n        self.set(key, value)\n        color.ColoredText.localize(\"config_success\")\n\n    def edit_locale(self):\n        text = self.get_full_input_localized(\n            ConfigKey.LOCALE,\n            self.get_str(ConfigKey.LOCALE),\n            self.get_default(ConfigKey.LOCALE),\n        )\n        color.ColoredText.localize(text)\n        all_locales = core.LocalManager.get_all_locales()\n        options = all_locales.copy() + [\"add_locale\", \"remove_locale\"]\n        value = dialog_creator.ChoiceInput.from_reduced(\n            options,\n            dialog=\"locale_dialog\",\n            single_choice=True,\n        ).single_choice()\n        if value is None:\n            return\n        value -= 1\n        if value == len(all_locales) + 1:  # remove_locale\n            options: list[str] = []\n            for locale in all_locales:\n                if locale.startswith(\"ext-\"):\n                    options.append(locale)\n\n            if not options:\n                color.ColoredText.localize(\n                    \"no_external_locales\",\n                )\n                return\n\n            options.append(\"cancel\")\n\n            choices, _ = dialog_creator.ChoiceInput.from_reduced(\n                options, dialog=\"locale_remove_dialog\"\n            ).multiple_choice()\n            if choices is None:\n                return\n            for choice in choices:\n                if choice == len(options) - 1:\n                    return\n                core.LocalManager.remove_locale(options[choice])\n                color.ColoredText.localize(\n                    \"locale_removed\",\n                    locale_name=options[choice],\n                )\n            return\n        elif value == len(all_locales):  # add_locale\n            if not core.GitHandler.is_git_installed():\n                color.ColoredText.localize(\n                    \"git_not_installed\",\n                )\n                return\n            git_repo = color.ColoredInput().localize(\"enter_locale_git_repo\").strip()\n            external_locale = core.ExternalLocale.from_git_repo(git_repo)\n            if external_locale is None:\n                color.ColoredText.localize(\n                    \"invalid_git_repo\",\n                )\n                return\n            locale_name = external_locale.get_full_name()\n            if locale_name in all_locales:\n                if not dialog_creator.YesNoInput().get_input_once(\n                    \"locale_already_exists\",\n                    {\"locale_name\": locale_name},\n                ):\n                    color.ColoredText.localize(\n                        \"locale_cancelled\",\n                    )\n                    return\n\n            external_locale.save()\n\n            value = locale_name\n            color.ColoredText.localize(\n                \"locale_added\",\n            )\n        else:\n            value = all_locales[value]\n        self.set(ConfigKey.LOCALE, value)\n        color.ColoredText.localize(\n            \"locale_changed\",\n            locale_name=value,\n        )\n        color.ColoredText.localize(\n            \"config_success\",\n        )\n\n    def edit_theme(self):\n        themes = core.ThemeHandler.get_all_themes()\n        current_theme = self.get_str(ConfigKey.THEME)\n        if current_theme not in themes:\n            current_theme = \"default\"\n        text = self.get_full_input_localized(\n            ConfigKey.THEME,\n            current_theme,\n            self.get_default(ConfigKey.THEME),\n        )\n        color.ColoredText.localize(text)\n        options = themes.copy() + [\"add_theme\", \"remove_theme\"]\n        value = dialog_creator.ChoiceInput.from_reduced(\n            options,\n            dialog=\"theme_dialog\",\n            single_choice=True,\n        ).single_choice()\n        if value is None:\n            return\n        value -= 1\n        if value == len(themes) + 1:  # remove_theme\n            options: list[str] = []\n            for theme in themes:\n                if theme.startswith(\"ext-\"):\n                    options.append(theme)\n\n            if not options:\n                color.ColoredText.localize(\n                    \"no_external_themes\",\n                )\n                return\n\n            options.append(\"cancel\")\n\n            choices, _ = dialog_creator.ChoiceInput.from_reduced(\n                options, dialog=\"theme_remove_dialog\"\n            ).multiple_choice()\n            if choices is None:\n                return\n            for choice in choices:\n                if choice == len(options) - 1:\n                    return\n                core.ThemeHandler.remove_theme(options[choice])\n                color.ColoredText.localize(\n                    \"theme_removed\",\n                    theme_name=options[choice],\n                )\n            return\n        elif value == len(themes):  # add_theme\n            if not core.GitHandler.is_git_installed():\n                color.ColoredText.localize(\n                    \"git_not_installed\",\n                )\n                return\n            git_repo = color.ColoredInput().localize(\"enter_theme_git_repo\").strip()\n            external_theme = core.ExternalTheme.from_git_repo(git_repo)\n            if external_theme is None:\n                color.ColoredText.localize(\n                    \"invalid_git_repo\",\n                )\n                return\n            theme_name = external_theme.get_full_name()\n            if theme_name in themes:\n                if not dialog_creator.YesNoInput().get_input_once(\n                    \"theme_already_exists\",\n                    {\"theme_name\": theme_name},\n                ):\n                    color.ColoredText.localize(\n                        \"theme_cancelled\",\n                    )\n                    return\n\n            external_theme.save()\n\n            value = theme_name\n            color.ColoredText.localize(\n                \"theme_added\",\n            )\n        else:\n            value = themes[value]\n        self.set(ConfigKey.THEME, value)\n        color.ColoredText.localize(\n            \"theme_changed\",\n            theme_name=value,\n        )\n\n    @staticmethod\n    def edit_config(_: Any = None):\n        config = core.core_data.config\n        features = list(ConfigKey)\n\n        choice = dialog_creator.ChoiceInput.from_reduced(\n            [key.value for key in features],\n            dialog=\"config_dialog\",\n            single_choice=True,\n        ).single_choice()\n        if choice is None:\n            return\n        choice -= 1\n        feature = features[choice]\n        print()\n        if isinstance(config.get(feature), bool):\n            core.core_data.config.edit_bool(feature)\n        elif isinstance(config.get(feature), int):\n            core.core_data.config.edit_int(feature)\n        elif feature == ConfigKey.LOCALE:\n            core.core_data.config.edit_locale()\n        elif feature == ConfigKey.THEME:\n            core.core_data.config.edit_theme()\n        elif feature == ConfigKey.GAME_DATA_REPO:\n            core.core_data.config.edit_game_data_repo()\n        elif isinstance(config.get(feature), str):\n            core.core_data.config.edit_str(feature)\n        print()\n"
  },
  {
    "path": "src/bcsfe/core/io/data.py",
    "content": "from __future__ import annotations\nimport base64\nimport enum\nfrom io import BytesIO\nimport struct\nimport typing\nfrom typing import Any, Literal\nfrom bcsfe import core\nimport datetime\n\n\nclass PaddingType(enum.Enum):\n    PKCS7 = enum.auto()\n    ZERO = enum.auto()\n    NONE = enum.auto()\n\n\nclass Data:\n    def __init__(\n        self, data: bytes | str | None | int | bool | Data | Any = None\n    ):\n        if isinstance(data, str):\n            self.data = data.encode(\"utf-8\")\n        elif isinstance(data, bytes):\n            self.data = data\n        elif isinstance(data, bool):\n            value = 1 if data else 0\n            self.data = str(value).encode(\"utf-8\")\n        elif isinstance(data, int):\n            self.data = str(data).encode(\"utf-8\")\n        elif isinstance(data, Data):\n            self.data = data.data\n        elif data is None:\n            self.data = b\"\"\n        elif hasattr(data, \"__bytes__\"):\n            self.data = bytes(data)\n        else:\n            raise TypeError(\n                f\"data must be bytes, str, int, bool, Data, or None, not {type(data)}\"\n            )\n        self.pos = 0\n        self.set_little_endiness()\n        self.buffer_enabled = False\n\n    def __bytes__(self) -> bytes:\n        return self.data\n\n    def __buffer__(self, flags: int, /) -> memoryview:\n        return memoryview(self.data)\n\n    @staticmethod\n    def from_hex(hex: str) -> Data:\n        return Data(bytes.fromhex(hex))\n\n    def enable_buffer(self):\n        self.data_buffer: list[bytes] = []\n        self.buffer_enabled = True\n\n    def end_buffer(self):\n        self.buffer_enabled = False\n        self.data = b\"\".join(self.data_buffer)\n        self.data_buffer = []\n\n    def set_endiness(self, endiness: Literal[\"<\", \">\"]):\n        self.endiness = endiness\n\n    def set_little_endiness(self):\n        self.set_endiness(\"<\")\n\n    def set_big_endiness(self):\n        self.set_endiness(\">\")\n\n    def is_empty(self) -> bool:\n        return len(self.data) == 0\n\n    def to_file(self, path: core.Path):\n        with open(path.path, \"wb\") as f:\n            f.write(self.data)\n\n    def copy(self) -> Data:\n        return Data(self.data)\n\n    @staticmethod\n    def from_file(path: core.Path) -> Data:\n        with open(path.path, \"rb\") as f:\n            return Data(f.read())\n\n    def set_pos(self, pos: int):\n        if pos < 0:\n            pos = len(self.data) + pos\n        self.pos = pos\n\n    def reset_pos(self):\n        self.pos = 0\n\n    def clear(self):\n        self.data = b\"\"\n        self.pos = 0\n\n    def get_pos(self) -> int:\n        return self.pos\n\n    def to_hex(self) -> str:\n        return self.data.hex()\n\n    def __len__(self) -> int:\n        return len(self.data)\n\n    def __add__(self, other: Data) -> Data:\n        return Data(self.data + other.data)\n\n    @typing.overload\n    def __getitem__(self, key: int) -> int:\n        pass\n\n    @typing.overload\n    def __getitem__(self, key: slice) -> Data:\n        pass\n\n    def __getitem__(self, key: int | slice) -> int | Data:\n        if isinstance(key, int):\n            return self.data[key]\n        elif isinstance(key, slice):  # type: ignore\n            return Data(self.data[key])\n        else:\n            raise TypeError(\"key must be int or slice\")\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, Data):\n            return self.data == other.data\n        else:\n            return False\n\n    def get_bytes(self) -> bytes:\n        return self.data\n\n    def read_bytes(self, length: int) -> bytes:\n        result = self.data[self.pos : self.pos + length]\n        self.pos += length\n        return result\n\n    def read_to_end(self, remaining_data: int = 0) -> bytes:\n        length = len(self.data) - self.pos - remaining_data\n        return self.read_bytes(length)\n\n    def read_int(self) -> int:\n        result = struct.unpack(f\"{self.endiness}i\", self.read_bytes(4))[0]\n        return result\n\n    def read_variable_length_int(self) -> int:\n        i = 0\n        for _ in range(4):\n            i3 = i << 7\n            read = self.read_ubyte()\n            i = i3 | (read & 0x7F)\n            if read & 0x80 == 0:\n                return i\n        return i\n\n    def write_variable_length_int(self, value: int):\n        value = int(value)\n        i2 = 0\n        i3 = 0\n        while value >= 128:\n            i2 |= ((value & 0x7F) | 0x8000) << (i3 * 8)\n            i3 += 1\n            value >>= 7\n        i4 = i2 | (value << (i3 * 8))\n        i5 = i3 + 1\n        for i6 in range(i5):\n            self.write_ubyte((i4 >> (((i5 - i6) - 1) * 8)) & 0xFF)\n\n    def read_int_list(self, length: int | None = None) -> list[int]:\n        if length is None:\n            length = self.read_int()\n        result: list[int] = []\n        for _ in range(length):\n            result.append(self.read_int())\n        return result\n\n    def read_bool_list(self, length: int | None = None) -> list[bool]:\n        if length is None:\n            length = self.read_int()\n        result: list[bool] = []\n        for _ in range(length):\n            result.append(self.read_bool())\n        return result\n\n    def read_string_list(self, length: int | None = None) -> list[str]:\n        if length is None:\n            length = self.read_int()\n        result: list[str] = []\n        for _ in range(length):\n            result.append(self.read_string())\n        return result\n\n    def read_byte_list(self, length: int | None = None) -> list[int]:\n        if length is None:\n            length = self.read_int()\n        result: list[int] = []\n        for _ in range(length):\n            result.append(self.read_byte())\n        return result\n\n    def read_short_list(self, length: int | None = None) -> list[int]:\n        if length is None:\n            length = self.read_int()\n        result: list[int] = []\n        for _ in range(length):\n            result.append(self.read_short())\n        return result\n\n    def read_uint(self) -> int:\n        result = struct.unpack(f\"{self.endiness}I\", self.read_bytes(4))[0]\n        return result\n\n    def read_short(self) -> int:\n        result = struct.unpack(f\"{self.endiness}h\", self.read_bytes(2))[0]\n        return result\n\n    def read_ushort(self) -> int:\n        result = struct.unpack(f\"{self.endiness}H\", self.read_bytes(2))[0]\n        return result\n\n    def read_byte(self) -> int:\n        result = struct.unpack(f\"{self.endiness}b\", self.read_bytes(1))[0]\n        return result\n\n    def read_ubyte(self) -> int:\n        result = struct.unpack(f\"{self.endiness}B\", self.read_bytes(1))[0]\n        return result\n\n    def read_float(self) -> float:\n        result = struct.unpack(f\"{self.endiness}f\", self.read_bytes(4))[0]\n        return result\n\n    def read_double(self) -> float:\n        result = struct.unpack(f\"{self.endiness}d\", self.read_bytes(8))[0]\n        return result\n\n    def read_string(self, length: int | None = None) -> str:\n        if length is None:\n            length = self.read_int()\n        result = self.read_bytes(length).decode(\"utf-8\")\n        return result\n\n    def read_utf8_string_by_char_length(self, length: int | None = None) -> str:\n        if length is None:\n            length = self.read_int()\n        if length == 0:\n            return \"\"\n        result_bytes = b\"\"\n        result_str = \"\"\n        while True:\n            byte = self.read_bytes(1)[0]\n            result_bytes += bytes([byte])\n            try:\n                result_str = result_bytes.decode(\"utf-8\")\n            except UnicodeDecodeError:\n                continue\n            if len(result_str) == length:\n                break\n        return result_str\n\n    def read_long(self) -> int:\n        result = struct.unpack(f\"{self.endiness}q\", self.read_bytes(8))[0]\n        return result\n\n    def read_ulong(self) -> int:\n        result = struct.unpack(f\"{self.endiness}Q\", self.read_bytes(8))[0]\n        return result\n\n    def read_date(self):\n        year = self.read_int()\n        month = self.read_int()\n        day = self.read_int()\n        hour = self.read_int()\n        minute = self.read_int()\n        second = self.read_int()\n        return datetime.datetime(year, month, day, hour, minute, second)\n\n    def write_date(self, date: datetime.datetime):\n        self.write_int(date.year)\n        self.write_int(date.month)\n        self.write_int(date.day)\n        self.write_int(date.hour)\n        self.write_int(date.minute)\n        self.write_int(date.second)\n\n    def write_bytes(self, data: bytes):\n        if self.buffer_enabled:\n            self.data_buffer.append(data)\n        else:\n            self.data += data\n        self.pos += len(data)\n\n    def write_int(self, value: int):\n        value = int(value)\n        self.write_bytes(struct.pack(f\"{self.endiness}i\", value))\n\n    def write_uint(self, value: int):\n        value = int(value)\n        self.write_bytes(struct.pack(f\"{self.endiness}I\", value))\n\n    def write_short(self, value: int):\n        value = int(value)\n        self.write_bytes(struct.pack(f\"{self.endiness}h\", value))\n\n    def write_ushort(self, value: int):\n        value = int(value)\n        self.write_bytes(struct.pack(f\"{self.endiness}H\", value))\n\n    def write_byte(self, value: int):\n        value = int(value)\n        self.write_bytes(struct.pack(f\"{self.endiness}b\", value))\n\n    def write_ubyte(self, value: int):\n        value = int(value)\n        self.write_bytes(struct.pack(f\"{self.endiness}B\", value))\n\n    def write_float(self, value: float):\n        self.write_bytes(struct.pack(f\"{self.endiness}f\", value))\n\n    def write_double(self, value: float):\n        self.write_bytes(struct.pack(f\"{self.endiness}d\", value))\n\n    def write_string(self, value: str, write_length: bool = True):\n        if write_length:\n            self.write_int(len(value.encode(\"utf-8\")))\n        self.write_bytes(value.encode(\"utf-8\"))\n\n    def write_long(self, value: int):\n        self.write_bytes(struct.pack(f\"{self.endiness}q\", value))\n\n    def write_ulong(self, value: int):\n        self.write_bytes(struct.pack(f\"{self.endiness}Q\", value))\n\n    def write_list(\n        self,\n        value: list[Any],\n        data_type: str,\n        empty_value: Any = None,\n        write_length: bool = True,\n        length: int | None = None,\n    ):\n        if length is None:\n            length = len(value)\n        if write_length:\n            self.write_int(length)\n        if length > len(value):\n            value += [empty_value] * (length - len(value))\n        elif length < len(value):\n            value = value[:length]\n        for item in value:\n            getattr(self, f\"write_{data_type}\")(item)\n\n    def write_int_list(\n        self,\n        value: list[int],\n        write_length: bool = True,\n        length: int | None = None,\n    ):\n        self.write_list(value, \"int\", 0, write_length, length)\n\n    def write_bool_list(\n        self,\n        value: list[bool],\n        write_length: bool = True,\n        length: int | None = None,\n    ):\n        self.write_list(value, \"bool\", False, write_length, length)\n\n    def write_string_list(\n        self,\n        value: list[str],\n        write_length: bool = True,\n        length: int | None = None,\n    ):\n        self.write_list(value, \"string\", \"\", write_length, length)\n\n    def write_byte_list(\n        self,\n        value: list[int],\n        write_length: bool = True,\n        length: int | None = None,\n    ):\n        self.write_list(value, \"byte\", 0, write_length, length)\n\n    def write_short_list(\n        self,\n        value: list[int],\n        write_length: bool = True,\n        length: int | None = None,\n    ):\n        self.write_list(value, \"short\", 0, write_length, length)\n\n    def read_bool(self) -> bool:\n        return self.read_byte() != 0\n\n    def write_bool(self, value: bool):\n        self.write_byte(int(value))\n\n    def read_int_tuple(self) -> tuple[int, int]:\n        return self.read_int(), self.read_int()\n\n    def read_int_tuple_list(\n        self, length: int | None = None\n    ) -> list[tuple[int, int]]:\n        if length is None:\n            length = self.read_int()\n        result: list[tuple[int, int]] = []\n        for _ in range(length):\n            result.append(self.read_int_tuple())\n        return result\n\n    def write_int_tuple(self, value: tuple[int, int]):\n        self.write_int(value[0])\n        self.write_int(value[1])\n\n    def write_int_tuple_list(\n        self,\n        value: list[tuple[int, int]],\n        write_length: bool = True,\n        length: int | None = None,\n    ):\n        self.write_list(value, \"int_tuple\", (0, 0), write_length, length)\n\n    def read_int_bool_dict(self, length: int | None = None) -> dict[int, bool]:\n        if length is None:\n            length = self.read_int()\n        result: dict[int, bool] = {}\n        for _ in range(length):\n            key = self.read_int()\n            value = self.read_bool()\n            result[key] = value\n        return result\n\n    def write_int_bool_dict(\n        self, value: dict[int, bool], write_length: bool = True\n    ):\n        if write_length:\n            self.write_int(len(value))\n        for key, item in value.items():\n            self.write_int(key)\n            self.write_bool(item)\n\n    def read_int_int_dict(self, length: int | None = None) -> dict[int, int]:\n        if length is None:\n            length = self.read_int()\n        result: dict[int, int] = {}\n        for _ in range(length):\n            key = self.read_int()\n            value = self.read_int()\n            result[key] = value\n        return result\n\n    def write_int_int_dict(\n        self, value: dict[int, int], write_length: bool = True\n    ):\n        if write_length:\n            self.write_int(len(value))\n        for key, item in value.items():\n            self.write_int(key)\n            self.write_int(item)\n\n    def read_int_double_dict(\n        self, length: int | None = None\n    ) -> dict[int, float]:\n        if length is None:\n            length = self.read_int()\n        result: dict[int, float] = {}\n        for _ in range(length):\n            key = self.read_int()\n            value = self.read_double()\n            result[key] = value\n        return result\n\n    def write_int_double_dict(\n        self, value: dict[int, float], write_length: bool = True\n    ):\n        if write_length:\n            self.write_int(len(value))\n        for key, item in value.items():\n            self.write_int(key)\n            self.write_double(item)\n\n    def read_short_bool_dict(\n        self, length: int | None = None\n    ) -> dict[int, bool]:\n        if length is None:\n            length = self.read_int()\n        result: dict[int, bool] = {}\n        for _ in range(length):\n            key = self.read_short()\n            value = self.read_bool()\n            result[key] = value\n        return result\n\n    def write_short_bool_dict(\n        self, value: dict[int, bool], write_length: bool = True\n    ):\n        if write_length:\n            self.write_int(len(value))\n        for key, item in value.items():\n            self.write_short(key)\n            self.write_bool(item)\n\n    def unpad_pkcs7(self) -> Data | None:\n        try:\n            pad = self.data[-1]\n        except IndexError:\n            return None\n        if pad > len(self.data):\n            return None\n        if self.data[-pad:] != bytes([pad] * pad):\n            return None\n        return Data(self.data[:-pad])\n\n    def split(self, separator: bytes, maxsplit: int = -1) -> list[Data]:\n        data_list: list[Data] = []\n        for line in self.data.split(separator, maxsplit):\n            data_list.append(Data(line))\n        return data_list\n\n    def to_int(self) -> int:\n        try:\n            return int(self.data.decode())\n        except ValueError:\n            return 0\n\n    def to_int_little(self) -> int:\n        return int.from_bytes(self.data, \"little\")\n\n    def to_str(self) -> str:\n        try:\n            return self.data.decode(encoding=\"utf-8\")\n        except UnicodeDecodeError:\n            return \"\"\n\n    def to_bool(self) -> bool:\n        return bool(self.to_int())\n\n    @staticmethod\n    def int_list_data_list(int_list: list[int]) -> list[Data]:\n        data_list: list[Data] = []\n        for integer in int_list:\n            data_list.append(Data(str(integer)))\n        return data_list\n\n    @staticmethod\n    def string_list_data_list(string_list: list[Any]) -> list[Data]:\n        data_list: list[Data] = []\n        for string in string_list:\n            data_list.append(Data(str(string)))\n        return data_list\n\n    @staticmethod\n    def data_list_int_list(data_list: list[Data]) -> list[int]:\n        int_list: list[int] = []\n        for data in data_list:\n            int_list.append(data.to_int())\n        return int_list\n\n    @staticmethod\n    def data_list_string_list(data_list: list[Data]) -> list[str]:\n        string_list: list[str] = []\n        for data in data_list:\n            string_list.append(data.to_str())\n        return string_list\n\n    def to_bytes(self) -> bytes:\n        return self.data\n\n    @staticmethod\n    def from_many(others: list[Data], joiner: Data | None = None) -> Data:\n        data_lst: list[bytes] = []\n        for other in others:\n            data_lst.append(other.data)\n        if joiner is None:\n            return Data(b\"\".join(data_lst))\n        else:\n            return Data(joiner.data.join(data_lst))\n\n    @staticmethod\n    def from_int_list(\n        int_list: list[int], endianess: Literal[\"little\", \"big\"]\n    ) -> Data:\n        bytes_data = b\"\"\n        for integer in int_list:\n            bytes_data += integer.to_bytes(4, endianess)\n        return Data(bytes_data)\n\n    def strip(self) -> Data:\n        return Data(self.data.strip())\n\n    def replace(self, old_data: Data, new_data: Data) -> Data:\n        return Data(self.data.replace(old_data.data, new_data.data))\n\n    def set(self, value: bytes | str | None | int | bool) -> None:\n        self.data = Data(value).data\n\n    def to_bytes_io(self) -> BytesIO:\n        return BytesIO(self.data)\n\n    def __repr__(self) -> str:\n        return f\"Data({self.data!r})\"\n\n    def __str__(self) -> str:\n        return self.to_str()\n\n    def to_base_64(self) -> str:\n        return base64.b64encode(self.data).decode()\n\n    @staticmethod\n    def from_base_64(string: str) -> Data:\n        return Data(base64.b64decode(string))\n\n    def to_csv(self, *args: Any, **kwargs: Any) -> core.CSV:\n        return core.CSV(self, *args, **kwargs)\n\n    def search(self, search_data: Data, start: int = 0) -> int:\n        return self.data.find(search_data.data, start)\n\n    def add_line(\n        self, line: Data | str | None | bytes | int | bool = None\n    ) -> Data:\n        line = Data(line)\n        self.data += line.data + b\"\\r\\n\"\n        return self\n\n\nclass PaddedInt:\n    def __init__(self, value: int, size: int):\n        self.value = value\n        self.size = size\n\n    def __int__(self):\n        return self.value\n\n    def __str__(self):\n        return str(self.value).zfill(self.size)\n\n    def __repr__(self):\n        return f\"PaddedInt({self.value}, {self.size})\"\n\n    def to_str(self):\n        return str(self)\n"
  },
  {
    "path": "src/bcsfe/core/io/git_handler.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\nclass Repo:\n    def __init__(self, url: str, output_error: bool = True):\n        self.url = url\n        self.output_error = output_error\n        self.success = self.clone()\n\n    def get_repo_name(self) -> str:\n        return self.url.split(\"/\")[-1]\n\n    def get_path(self) -> core.Path:\n        path = GitHandler.get_repo_folder().add(self.get_repo_name())\n        path.generate_dirs()\n        return path\n\n    def run_cmd(self, cmd: str) -> bool:\n        result = core.Command(cmd).run()\n        success = result.exit_code == 0\n        if not success and self.output_error:\n            color.ColoredText.localize(\"failed_to_run_git_cmd\", cmd=cmd)\n        return success\n\n    def clone_to_temp(self, path: core.Path) -> bool:\n        cmd = f\"git clone {self.url} {path}\"\n        return self.run_cmd(cmd)\n\n    def clone(self) -> bool:\n        if self.is_cloned():\n            return True\n        cmd = f\"git clone {self.url} {self.get_path()}\"\n        success = self.run_cmd(cmd)\n        if not success:\n            self.get_path().remove()\n        return success\n\n    def pull(self) -> bool:\n        cmd = f\"git -C {self.get_path()} pull\"\n        return self.run_cmd(cmd)\n\n    def fetch(self) -> bool:\n        cmd = f\"git -C {self.get_path()} fetch\"\n        return self.run_cmd(cmd)\n\n    def get_file(self, file_path: core.Path) -> core.Data | None:\n        path = self.get_path().add(file_path)\n        try:\n            return path.read()\n        except FileNotFoundError:\n            return None\n\n    def get_temp_file(self, temp_folder: core.Path, file_path: core.Path) -> core.Data:\n        path = temp_folder.add(file_path)\n        return path.read()\n\n    def get_folder(self, folder_path: core.Path) -> core.Path | None:\n        path = self.get_path().add(folder_path)\n        if path.exists():\n            return path\n        return None\n\n    def is_cloned(self) -> bool:\n        return (\n            len(self.get_path().get_dirs()) > 0\n            or len(self.get_path().get_paths_dir()) > 0\n        )\n\n\nclass GitHandler:\n    @staticmethod\n    def get_repo_folder() -> core.Path:\n        repo_folder = core.Path.get_data_folder().add(\"repos\")\n        repo_folder.generate_dirs()\n        return repo_folder\n\n    def get_repo(self, repo_url: str, output_error: bool = True) -> Repo | None:\n        repo = Repo(repo_url)\n        if repo.success:\n            return repo\n        if output_error:\n            color.ColoredText.localize(\"failed_to_get_repo\", url=repo_url)\n        return None\n\n    @staticmethod\n    def is_git_installed() -> bool:\n        cmd = \"git --version\"\n        return core.Command(cmd).run().exit_code == 0\n"
  },
  {
    "path": "src/bcsfe/core/io/json_file.py",
    "content": "from __future__ import annotations\nimport json\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass JsonFile:\n    def __init__(self, data: core.Data):\n        self.json = json.loads(data.data)\n\n    @staticmethod\n    def from_path(path: core.Path) -> JsonFile:\n        return JsonFile(path.read())\n\n    @staticmethod\n    def from_object(js: Any) -> JsonFile:\n        return JsonFile(core.Data(json.dumps(js)))\n\n    @staticmethod\n    def from_data(data: core.Data) -> JsonFile:\n        return JsonFile(data)\n\n    def to_data(self, indent: int | None = 4) -> core.Data:\n        return core.Data(json.dumps(self.json, indent=indent))\n\n    def to_file(self, path: core.Path) -> None:\n        path.write(self.to_data())\n\n    def to_object(self) -> Any:\n        return self.json\n\n    def get(self, key: str) -> Any:\n        return self.json[key]\n\n    def set(self, key: str, value: Any) -> None:\n        self.json[key] = value\n\n    def __str__(self) -> str:\n        return str(self.json)\n\n    def __getitem__(self, key: str) -> Any:\n        return self.json[key]\n\n    def __setitem__(self, key: str, value: Any) -> None:\n        self.json[key] = value\n"
  },
  {
    "path": "src/bcsfe/core/io/path.py",
    "content": "from __future__ import annotations\nimport glob\nimport os\nimport shutil\n\nfrom bcsfe import core\nimport re\n\n\nclass Path:\n    def __init__(self, path: str = \"\", is_relative: bool = False):\n        if isinstance(path, Path):\n            path = path.path\n        if is_relative:\n            self.path = self.get_relative_path(path)\n        else:\n            self.path = path\n\n    def is_relative(self) -> bool:\n        return not os.path.isabs(self.path)\n\n    @staticmethod\n    def get_root() -> Path:\n        return Path(os.sep)\n\n    def get_relative_path(self, path: str) -> str:\n        return os.path.join(self.get_files_folder().path, path)\n\n    @staticmethod\n    def get_files_folder() -> Path:\n        file = Path(os.path.realpath(__file__))\n        if file.get_extension() == \"pyc\":\n            path = file.parent().parent().parent().parent().add(\"files\")\n        else:\n            path = file.parent().parent().parent().add(\"files\")\n        return path\n\n    def strip_trailing_slash(self) -> Path:\n        return Path(self.path.rstrip(\"/\"))\n\n    def open(self):\n        self.generate_dirs()\n        if os.name == \"nt\":\n            os.startfile(self.path)  # type: ignore\n        elif os.name == \"posix\":\n            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:''\"\n            core.Command(cmd, display_output=False).run_in_thread()\n        elif os.name == \"mac\":\n            core.Command(f\"open {self.path}\", display_output=False).run()\n        else:\n            raise OSError(\"Unknown OS\")\n\n    def open_file(self):\n        os_name = os.name\n        if os_name == \"nt\":\n            os.startfile(self.path)  # type: ignore\n        elif os_name == \"posix\":\n            cmd = f\"xdg-open {self.path}\"\n            core.Command(cmd, display_output=False).run_in_thread()\n        elif os_name == \"mac\":\n            core.Command(f\"open {self.path}\", display_output=False).run()\n        else:\n            raise OSError(\"Unknown OS\")\n\n    def run(self, arg: str = \"\", display_output: bool = False) -> core.CommandResult:\n        cmd_text = self.path + \" \" + arg\n        cmd = core.Command(cmd_text, display_output=display_output)\n        return cmd.run()\n\n    def to_str(self) -> str:\n        return self.path\n\n    def to_str_forwards(self) -> str:\n        return self.path.replace(\"\\\\\", \"/\")\n\n    @staticmethod\n    def get_data_folder(app_name: str = \"bcsfe\") -> Path:\n        os_name = os.name\n        if os_name == \"nt\":\n            path = Path.join(os.environ[\"USERPROFILE\"], \"Documents\", app_name)\n        elif os_name == \"posix\":\n            data_home = os.environ.get(\"XDG_DATA_HOME\")\n            if data_home is None:\n                path = Path.join(os.environ[\"HOME\"], \".local\", \"share\", app_name)\n            else:\n                path = Path.join(data_home, app_name)\n        elif os_name == \"mac\":\n            path = Path.join(os.environ[\"HOME\"], \"Documents\", app_name)\n        else:\n            raise OSError(\"Unknown OS\")\n        path.generate_dirs()\n        return path\n\n    @staticmethod\n    def get_config_folder(app_name: str = \"bcsfe\") -> Path:\n        os_name = os.name\n        if os_name != \"posix\":\n            return Path.get_data_folder()\n        data_home = os.environ.get(\"XDG_CONFIG_HOME\")\n        if data_home is None:\n            path = Path.join(os.environ[\"HOME\"], \".config\", app_name)\n        else:\n            path = Path.join(data_home, app_name)\n        path.generate_dirs()\n        return path\n\n    @staticmethod\n    def get_state_folder(app_name: str = \"bcsfe\") -> Path:\n        os_name = os.name\n        if os_name != \"posix\":\n            return Path.get_data_folder()\n        data_home = os.environ.get(\"XDG_STATE_HOME\")\n        if data_home is None:\n            path = Path.join(os.environ[\"HOME\"], \".local\", \"state\", app_name)\n        else:\n            path = Path.join(data_home, app_name)\n        path.generate_dirs()\n        return path\n\n    def is_empty(self) -> bool:\n        return self.path == \"\"\n\n    def generate_dirs(self: Path) -> Path:\n        if self.is_empty():\n            return self\n        if not self.exists():\n            try:\n                self.__make_dirs()\n            except OSError as e:\n                print(e, self)\n        return self\n\n    def create(self) -> Path:\n        if not self.exists():\n            self.write(core.Data(\"test\"))\n        return self\n\n    def exists(self) -> bool:\n        return os.path.exists(self.path)\n\n    def __make_dirs(self) -> Path:\n        os.makedirs(self.path)\n        return self\n\n    def basename(self) -> str:\n        return os.path.basename(self.path)\n\n    @staticmethod\n    def join(*paths: str | Path) -> Path:\n        _paths: list[str] = [str(path) for path in paths]\n        return Path(os.path.join(*_paths))\n\n    def add(self, *paths: str | Path) -> Path:\n        _paths: list[str] = [str(path) for path in paths]\n        return Path(os.path.join(self.path, *_paths))\n\n    def strip_leading_slash(self) -> Path:\n        return Path(self.path.lstrip(\"/\").lstrip(\"\\\\\"))\n\n    def __str__(self) -> str:\n        return self.path\n\n    def __repr__(self) -> str:\n        return self.path\n\n    def remove_tree(self, ignoreErrors: bool = False) -> Path:\n        if self.exists():\n            shutil.rmtree(self.path, ignore_errors=ignoreErrors)\n        return self\n\n    def remove(self):\n        if self.exists():\n            if self.is_directory():\n                self.remove_tree()\n            else:\n                os.remove(self.path)\n\n    def has_files(self) -> bool:\n        return len(os.listdir(self.path)) > 0\n\n    def copy(self, target: Path):\n        if self.exists():\n            if self.is_directory():\n                self.copy_tree(target)\n            else:\n                try:\n                    target.parent().generate_dirs()\n                    shutil.copy(self.path, target.path)\n                except shutil.SameFileError:\n                    pass\n        else:\n            raise FileNotFoundError(f\"File not found: {self.path}\")\n\n    def copy_thread(self, target: Path):\n        core.Thread(\"copy\", self.copy, (target,)).start()\n\n    def copy_tree(self, target: Path):\n        if target.exists():\n            target.remove_tree()\n        if self.exists():\n            target.parent().generate_dirs()\n            shutil.copytree(self.path, target.path)\n\n    def read(self, create: bool = False) -> core.Data:\n        if self.exists():\n            return core.Data.from_file(self)\n        elif create:\n            self.write(core.Data())\n            return self.read()\n        else:\n            raise FileNotFoundError(f\"File not found: {self.path}\")\n\n    def write(self, data: core.Data):\n        data.to_file(self)\n\n    def get_paths_dir(self, regex: str | None = None) -> list[Path]:\n        if self.exists():\n            if regex is None:\n                return [self.add(file) for file in os.listdir(self.path)]\n            else:\n                files: list[Path] = []\n                for file in os.listdir(self.path):\n                    if re.search(regex, file):\n                        files.append(self.add(file))\n                return files\n        return []\n\n    def get_files(self, regex: str | None = None) -> list[Path]:\n        return [file for file in self.get_paths_dir(regex) if file.is_file()]\n\n    def is_file(self) -> bool:\n        return os.path.isfile(self.path)\n\n    def get_dirs(self) -> list[\"Path\"]:\n        return [file for file in self.get_paths_dir() if file.is_directory()]\n\n    def glob(self, pattern: str, recursive: bool = False) -> list[Path]:\n        return [\n            Path(path)\n            for path in glob.glob(self.add(pattern).path, recursive=recursive)\n        ]\n\n    def strip_path_from(self, path: Path) -> Path:\n        return Path(self.path.replace(path.path, \"\")).strip_leading_slash()\n\n    def is_directory(self) -> bool:\n        return os.path.isdir(self.path)\n\n    def change_name(self, name: str) -> Path:\n        return self.parent().add(name)\n\n    def rename(self, name: str, overwrite: bool = False):\n        if not self.exists():\n            raise FileNotFoundError(f\"File not found: {self.path}\")\n        new_path = self.change_name(name)\n        if new_path.path == self.path:\n            return\n        if new_path.exists():\n            if overwrite:\n                new_path.remove()\n            else:\n                raise FileExistsError(f\"File already exists: {new_path}\")\n        os.rename(self.path, new_path.path)\n        self.path = new_path.path\n\n    def parent(self) -> Path:\n        return Path(os.path.dirname(self.path))\n\n    def change_extension(self, extension: str) -> Path:\n        if extension.startswith(\".\"):\n            extension = extension[1:]\n        return Path(self.path.rsplit(\".\", 1)[0] + \".\" + extension)\n\n    def remove_extension(self) -> Path:\n        return Path(self.path.rsplit(\".\", 1)[0])\n\n    def get_extension(self) -> str:\n        try:\n            return self.path.rsplit(\".\", 1)[1]\n        except IndexError:\n            return \"\"\n\n    def get_file_name(self) -> str:\n        return os.path.basename(self.path)\n\n    def get_file_name_path(self) -> Path:\n        return Path(self.get_file_name())\n\n    def get_file_name_without_extension(self) -> str:\n        return self.get_file_name().rsplit(\".\", 1)[0]\n\n    def get_file_size(self) -> int:\n        return os.path.getsize(self.path)\n\n    def get_absolute_path(self) -> Path:\n        return Path(os.path.abspath(self.path))\n\n    def copy_object(self) -> Path:\n        return Path(self.path)\n"
  },
  {
    "path": "src/bcsfe/core/io/root_handler.py",
    "content": "from __future__ import annotations\nfrom bcsfe import core\nimport tempfile\n\n\nclass PackageNameNotSet(Exception):\n    pass\n\n\nclass RootHandler:\n    def __init__(self):\n        self.package_name = None\n\n    def is_android(self) -> bool:\n        return core.Path.get_root().add(\"system\").exists()\n\n    def set_package_name(self, package_name: str):\n        self.package_name = package_name\n\n    def is_rooted(self) -> bool:\n        try:\n            core.Path.get_root().add(\"data\").add(\"data\").get_dirs()\n        except PermissionError:\n            return False\n        return True\n\n    def get_battlecats_packages(self) -> list[str]:\n        packages = core.Path.get_root().add(\"data\").add(\"data\").get_dirs()\n        packages = [\n            package.basename()\n            for package in packages\n            if package.add(\"files\").add(\"SAVE_DATA\").exists()\n        ]\n        return packages\n\n    def get_package_name(self) -> str:\n        if self.package_name is None:\n            raise PackageNameNotSet(\"Package name is not set\")\n        return self.package_name\n\n    def get_battlecats_path(self) -> core.Path:\n        return core.Path.get_root().add(\"data\").add(\"data\").add(self.get_package_name())\n\n    def get_battlecats_save_path(self) -> core.Path:\n        return self.get_battlecats_path().add(\"files\").add(\"SAVE_DATA\")\n\n    def save_battlecats_save(self, local_path: core.Path) -> core.CommandResult:\n        self.get_battlecats_save_path().copy(local_path)\n        return core.CommandResult.create_success()\n\n    def load_battlecats_save(self, local_path: core.Path) -> core.CommandResult:\n        local_path.copy(self.get_battlecats_save_path())\n        return core.CommandResult.create_success()\n\n    def close_game(self) -> core.CommandResult:\n        cmd = core.Command(\n            f\"sudo pkill -f {self.get_package_name()}\",\n        )\n        return cmd.run()\n\n    def run_game(self) -> core.CommandResult:\n        cmd = core.Command(\n            f\"sudo monkey -p {self.get_package_name()} -c android.intent.category.LAUNCHER 1\",\n        )\n        return cmd.run()\n\n    def rerun_game(self) -> core.CommandResult:\n        result = self.close_game()\n        if not result.success:\n            return result\n        result = self.run_game()\n        if not result.success:\n            return result\n\n        return core.CommandResult.create_success()\n\n    def save_locally(\n        self, local_path: core.Path | None = None\n    ) -> tuple[core.Path | None, core.CommandResult]:\n        if local_path is None:\n            local_path = core.Path.get_data_folder().add(\"saves\").add(\"SAVE_DATA\")\n        local_path.parent().generate_dirs()\n        result = self.save_battlecats_save(local_path)\n        if not result.success:\n            return None, result\n\n        return local_path, result\n\n    def load_locally(self, local_path: core.Path) -> core.CommandResult:\n        success = self.load_battlecats_save(local_path)\n        if not success:\n            return core.CommandResult.create_failure()\n\n        success = self.rerun_game()\n        if not success:\n            return core.CommandResult.create_failure()\n\n        return core.CommandResult.create_success()\n\n    def load_save(\n        self, save: core.SaveFile, rerun_game: bool = True\n    ) -> core.CommandResult:\n        with tempfile.TemporaryDirectory() as temp_dir:\n            local_path = core.Path(temp_dir).add(\"SAVE_DATA\")\n            save.to_data().to_file(local_path)\n            result = self.load_battlecats_save(local_path)\n            if not result.success:\n                return result\n        if rerun_game:\n            result = self.rerun_game()\n\n        return result\n"
  },
  {
    "path": "src/bcsfe/core/io/save.py",
    "content": "from __future__ import annotations\nimport base64\nfrom typing import Any\nfrom bcsfe import core, __version__, cli\nimport datetime\n\nfrom bcsfe.cli.color import ColoredText\nfrom bcsfe.core.io.config import ConfigKey\n\n\nclass SaveError(Exception):\n    pass\n\n\nclass CantDetectSaveCCError(SaveError):\n    pass\n\n\nclass SaveFileInvalid(SaveError):\n    pass\n\n\nclass FailedToLoadError(SaveError):\n    pass\n\n\nclass FailedToSaveError(SaveError):\n    pass\n\n\nclass SaveFile:\n    def __init__(\n        self,\n        dt: core.Data | None = None,\n        cc: core.CountryCode | None = None,\n        load: bool = True,\n        gv: core.GameVersion | None = None,\n        package_name: str | None = None,\n    ):\n        self.package_name = package_name\n        self.save_path: core.Path | None = None\n        if dt is None:\n            self.data = core.Data()\n        else:\n            self.data = dt\n        detected_cc = self.detect_cc()\n        if detected_cc is None:\n            if cc is None:\n                raise CantDetectSaveCCError(\n                    core.core_data.local_manager.get_key(\"cant_detect_cc\")\n                )\n            self.cc = cc\n        else:\n            self.cc = detected_cc\n\n        self.used_storage = False\n\n        self.localizable: core.Localizable | None = None\n\n        self.init_save(gv)\n\n        if dt is not None and load:\n            self.load_wrapper()\n\n    def get_localizable(self) -> core.Localizable:\n        if self.localizable is None:\n            self.localizable = core.Localizable(self)\n        return self.localizable\n\n    def load_save_file(self, other: SaveFile):\n        self.data = other.data\n        self.cc = other.cc\n        self.game_version = other.game_version\n        self.init_save(other.game_version)\n        self.load_wrapper()\n\n    def detect_cc(self) -> core.CountryCode | None:\n        for cc in core.CountryCode.get_all():\n            self.cc = cc\n            if self.verify_hash():\n                return cc\n        return None\n\n    def get_salt(self) -> str:\n        \"\"\"Get the salt for the save file. This is used for hashing the save file.\n\n        Returns:\n            str: The salt\n        \"\"\"\n        salt = f\"battlecats{self.cc.get_patching_code()}\"\n        return salt\n\n    def get_current_hash(self) -> str:\n        \"\"\"Get the current hash for the save file. This is used for hashing the save file.\n\n        Returns:\n            str: The current hash\n        \"\"\"\n        self.data.reset_pos()\n        self.data.set_pos(-32)\n        hash = self.data.read_string(32)\n        return hash\n\n    def get_new_hash(self, existing_hash: bool = True) -> str:\n        \"\"\"Get the new hash for the save file. This is used for hashing the save file.\n\n        Returns:\n            str: The new hash\n        \"\"\"\n        salt = self.get_salt()\n        self.data.reset_pos()\n        if existing_hash:\n            data_to_hash = self.data.read_bytes(len(self.data) - 32)\n        else:\n            data_to_hash = self.data.read_bytes(len(self.data))\n        salted_data = core.Data(salt.encode(\"utf-8\") + data_to_hash)\n        hash = core.Hash(core.HashAlgorithm.MD5).get_hash(salted_data)\n        return hash.to_hex()\n\n    def set_hash(self, add: bool = False):\n        \"\"\"Set the hash of the save file.\"\"\"\n        hash = self.get_new_hash(existing_hash=not add)\n        if not add:\n            self.data.set_pos(-32)\n        else:\n            self.data.set_pos(len(self.data))\n        self.data.write_string(hash, write_length=False)\n\n    def verify_hash(self) -> bool:\n        \"\"\"Verify the hash of the save file.\n\n        Returns:\n            bool: Whether the hash is valid\n        \"\"\"\n        return self.get_current_hash() == self.get_new_hash()\n\n    def load_wrapper(self):\n        try:\n            self.load()\n        except Exception as e:\n            ignore_error = core.core_data.config.get_bool(ConfigKey.IGNORE_PARSE_ERROR)\n            if not ignore_error:\n                raise FailedToLoadError(\n                    core.core_data.local_manager.get_key(\"failed_to_load_save\")\n                ) from e\n            else:\n                from traceback import format_exc\n\n                ColoredText.localize(\"parse_ignored_error\", error=format_exc())\n\n    def set_gv(self, gv: core.GameVersion):\n        self.game_version = gv\n\n    def set_cc(self, cc: core.CountryCode):\n        self.cc = cc\n        self.set_package_name(None)\n\n    def set_package_name(self, package_name: str | None):\n        self.package_name = package_name\n\n    def load(self):\n        \"\"\"Load the save file. For most of this stuff I have no idea what it is used for\"\"\"\n\n        self.data.reset_pos()\n        self.dst_index = 0\n\n        self.dsts: list[bool] = []\n\n        self.game_version: core.GameVersion = core.GameVersion(self.data.read_int())\n\n        if self.game_version >= 10 or self.not_jp():\n            self.ub1 = self.data.read_bool()\n\n        self.mute_bgm = self.data.read_bool()\n        self.mute_se = self.data.read_bool()\n\n        self.catfood = self.data.read_int()\n        self.current_energy = self.data.read_int()\n\n        year = self.data.read_int()\n        self.year = self.data.read_int()\n\n        month = self.data.read_int()\n        self.month = self.data.read_int()\n\n        day = self.data.read_int()\n        self.day = self.data.read_int()\n\n        self.timestamp = self.data.read_double()\n\n        hour = self.data.read_int()\n        minute = self.data.read_int()\n        second = self.data.read_int()\n\n        self.read_dst()\n\n        self.date = datetime.datetime(year, month, day, hour, minute, second)\n\n        self.ui1 = self.data.read_int()\n        self.stamp_value_save = self.data.read_int()\n        self.ui2 = self.data.read_int()\n\n        self.upgrade_state = self.data.read_int()\n\n        self.xp = self.data.read_int()\n        self.tutorial_state = self.data.read_int()\n\n        self.ui3 = self.data.read_int()\n        self.koreaSuperiorTreasureState = self.data.read_int()\n\n        self.unlock_popups_11 = self.data.read_int_list(3)\n        self.ui5 = self.data.read_int()\n        self.unlock_enemy_guide = self.data.read_int()\n        self.ui6 = self.data.read_int()\n        self.ub0 = self.data.read_bool()\n        self.ui7 = self.data.read_int()\n        self.cleared_eoc_1 = self.data.read_int()\n        self.ui8 = self.data.read_int()\n        self.unlocked_ending = self.data.read_int()\n\n        self.lineups = core.LineUps.read(self.data, self.game_version)\n\n        self.stamp_data = core.StampData.read(self.data)\n        self.story = core.StoryChapters.read(self.data)\n\n        if 20 <= self.game_version and self.game_version <= 25:\n            self.enemy_guide = self.data.read_int_list(231)\n        else:\n            self.enemy_guide = self.data.read_int_list()\n\n        self.cats = core.Cats.read_unlocked(self.data, self.game_version)\n        self.cats.read_upgrade(self.data, self.game_version)\n        self.cats.read_current_form(self.data, self.game_version)\n\n        self.special_skills = core.SpecialSkills.read_upgrades(self.data)\n        if self.game_version <= 25:\n            self.menu_unlocks = self.data.read_int_list(5)\n            self.unlock_popups_0 = self.data.read_int_list(5)\n        elif self.game_version == 26:\n            self.menu_unlocks = self.data.read_int_list(6)\n            self.unlock_popups_0 = self.data.read_int_list(6)\n        else:\n            self.menu_unlocks = self.data.read_int_list()\n            self.unlock_popups_0 = self.data.read_int_list()\n\n        self.battle_items = core.BattleItems.read_items(self.data)\n\n        if self.game_version <= 26:\n            self.new_dialogs_2 = self.data.read_int_list(17)\n        else:\n            self.new_dialogs_2 = self.data.read_int_list()\n\n        self.uil1 = self.data.read_int_list(length=20)\n        self.moneko_bonus = self.data.read_int_list(length=1)\n        self.daily_reward_initialized = self.data.read_int_list(length=1)\n\n        self.battle_items.read_locked_items(self.data)\n\n        self.read_dst()\n        self.date_2 = self.data.read_date()\n\n        self.story.read_treasure_festival(self.data)\n\n        self.read_dst()\n        self.date_3 = self.data.read_date()\n\n        if self.game_version <= 37:\n            self.ui0 = self.data.read_int()\n\n        self.stage_unlock_cat_value = self.data.read_int()\n        self.show_ending_value = self.data.read_int()\n        self.chapter_clear_cat_unlock = self.data.read_int()\n        self.ui9 = self.data.read_int()\n        self.ios_android_month = self.data.read_int()\n        self.ui10 = self.data.read_int()\n        self.save_data_4_hash = self.data.read_string()\n\n        self.mysale = core.MySale.read_bonus_hash(self.data)\n        self.chara_flags = self.data.read_int_list(length=2)\n\n        if self.game_version <= 37:\n            self.uim1 = self.data.read_int()\n            self.ubm1 = self.data.read_bool()\n\n        self.chara_flags_2 = self.data.read_int_list(length=2)\n\n        self.normal_tickets = self.data.read_int()\n        self.rare_tickets = self.data.read_int()\n\n        self.cats.read_gatya_seen(self.data, self.game_version)\n        self.special_skills.read_gatya_seen(self.data)\n        self.cats.read_storage(self.data, self.game_version)\n\n        self.event_stages = core.EventChapters.read(self.data, self.game_version)\n        self.itf1_ending = self.data.read_int()\n        self.continue_flag = self.data.read_int()\n        if 20 <= self.game_version:\n            self.unlock_popups_8 = self.data.read_int_list(length=36)\n\n        if 20 <= self.game_version and self.game_version <= 25:\n            self.unit_drops = self.data.read_int_list(length=110)\n        elif 26 <= self.game_version:\n            self.unit_drops = self.data.read_int_list()\n\n        self.gatya = core.Gatya.read_rare_normal_seed(self.data, self.game_version)\n\n        self.get_event_data = self.data.read_bool()\n        self.achievements = self.data.read_bool_list(length=7)\n\n        self.os_value = self.data.read_int()\n\n        self.read_dst()\n        self.date_4 = self.data.read_date()\n\n        self.gatya.read2(self.data)\n\n        if self.not_jp():\n            self.player_id = self.data.read_string()\n\n        self.order_ids = self.data.read_string_list()\n\n        if self.not_jp():\n            self.g_timestamp = self.data.read_double()\n            self.g_servertimestamp = self.data.read_double()\n            self.m_gettimesave = self.data.read_double()\n            self.usl1 = self.data.read_string_list()\n            self.energy_notification = self.data.read_bool()\n            self.full_gameversion = self.data.read_int()\n\n        self.lineups.read_2(self.data, self.game_version)\n        self.event_stages.read_legend_restrictions(self.data, self.game_version)\n\n        if self.game_version <= 37:\n            self.uil2 = self.data.read_int_list(length=7)\n            self.uil3 = self.data.read_int_list(length=7)\n            self.uil4 = self.data.read_int_list(length=7)\n\n        self.g_timestamp_2 = self.data.read_double()\n        self.g_servertimestamp_2 = self.data.read_double()\n        self.m_gettimesave_2 = self.data.read_double()\n        self.unknown_timestamp = self.data.read_double()\n        self.gatya.read_trade_progress(self.data)\n\n        if self.game_version <= 37:\n            self.usl2 = self.data.read_string_list()\n\n        if self.not_jp():\n            self.m_dGetTimeSave2 = self.data.read_double()\n            self.ui11 = 0\n        else:\n            self.ui11 = self.data.read_int()\n\n        if 20 <= self.game_version and self.game_version <= 25:\n            self.ubl1 = self.data.read_bool_list(length=12)\n        elif 26 <= self.game_version and self.game_version < 39:\n            self.ubl1 = self.data.read_bool_list()\n\n        self.cats.read_max_upgrade_levels(self.data, self.game_version)\n        self.special_skills.read_max_upgrade_levels(self.data)\n\n        self.user_rank_rewards = core.UserRankRewards.read(self.data, self.game_version)\n\n        if not self.not_jp():\n            self.m_dGetTimeSave2 = self.data.read_double()\n\n        self.cats.read_unlocked_forms(self.data, self.game_version)\n\n        self.transfer_code = self.data.read_string()\n        self.confirmation_code = self.data.read_string()\n        self.transfer_flag = self.data.read_bool()\n\n        if 20 <= self.game_version:\n            self.item_reward_stages = core.ItemRewardChapters.read(\n                self.data, self.game_version\n            )\n\n            self.timed_score_stages = core.TimedScoreChapters.read(\n                self.data, self.game_version\n            )\n            self.inquiry_code = self.data.read_string()\n            self.officer_pass = core.OfficerPass.read(self.data)\n            self.has_account = self.data.read_byte()\n            self.backup_state = self.data.read_int()\n\n            if self.not_jp():\n                self.ub2 = self.data.read_bool()\n\n            assert self.data.read_int() == 44\n\n            self.itf1_complete = self.data.read_int()\n\n            self.story.read_itf_timed_scores(self.data)\n\n            self.title_chapter_bg = self.data.read_int()\n\n            if self.game_version > 26:\n                self.combo_unlocks = self.data.read_int_list()\n\n            self.combo_unlocked_10k_ur = self.data.read_bool()\n\n            assert self.data.read_int() == 45\n\n        if 21 <= self.game_version:\n            assert self.data.read_int() == 46\n\n            self.gatya.read_event_seed(self.data, self.game_version)\n            if self.game_version < 34:\n                self.event_capsules = self.data.read_int_list(length=100)\n                self.event_capsules_counter = self.data.read_int_list(length=100)\n            else:\n                self.event_capsules = self.data.read_int_list()\n                self.event_capsules_counter = self.data.read_int_list()\n\n            assert self.data.read_int() == 47\n\n        if 22 <= self.game_version:\n            assert self.data.read_int() == 48\n\n        if 23 <= self.game_version:\n            if not self.not_jp():\n                self.energy_notification = self.data.read_bool()\n\n            self.m_dGetTimeSave3 = self.data.read_double()\n            if self.game_version < 26:\n                self.gatya_seen_lucky_drops = self.data.read_int_list(length=44)\n            else:\n                self.gatya_seen_lucky_drops = self.data.read_int_list()\n            self.show_ban_message = self.data.read_bool()\n            self.catfood_beginner_purchased = self.data.read_bool_list(length=3)\n            self.next_week_timestamp = self.data.read_double()\n            self.catfood_beginner_expired = self.data.read_bool_list(length=3)\n            self.rank_up_sale_value = self.data.read_int()\n\n            assert self.data.read_int() == 49\n\n        if 24 <= self.game_version:\n            assert self.data.read_int() == 50\n\n        if 25 <= self.game_version:\n            assert self.data.read_int() == 51\n\n        if 26 <= self.game_version:\n            self.cats.read_catguide_collected(self.data)\n\n            assert self.data.read_int() == 52\n\n        if 27 <= self.game_version:\n            self.time_since_time_check_cumulative = self.data.read_double()\n            self.server_timestamp = self.data.read_double()\n            self.last_checked_energy_recovery_time = self.data.read_double()\n            self.time_since_check = self.data.read_double()\n            self.last_checked_expedition_time = self.data.read_double()\n\n            self.catfruit = self.data.read_int_list()\n            self.cats.read_fourth_forms(self.data)\n            self.cats.read_catseyes_used(self.data)\n            self.catseyes = self.data.read_int_list()\n            self.catamins = self.data.read_int_list()\n            self.gamatoto = core.Gamatoto.read(self.data)\n\n            self.unlock_popups_6 = self.data.read_bool_list()\n            self.ex_stages = core.ExChapters.read(self.data)\n\n            assert self.data.read_int() == 53\n\n        if 29 <= self.game_version:\n            self.gamatoto.read_2(self.data)\n            assert self.data.read_int() == 54\n            self.item_pack = core.ItemPack.read(self.data)\n            assert self.data.read_int() == 54\n\n        if self.game_version >= 30:\n            self.gamatoto.read_skin(self.data)\n            self.platinum_tickets = self.data.read_int()\n            self.logins = core.LoginBonus.read(self.data, self.game_version)\n            if self.game_version < 101000:\n                self.reset_item_reward_flags = self.data.read_bool_list()\n\n            self.reward_remaining_time = self.data.read_double()\n            self.last_checked_reward_time = self.data.read_double()\n            self.announcements = self.data.read_int_tuple_list(length=16)\n            self.backup_counter = self.data.read_int()\n            self.ui12 = self.data.read_int()\n            self.ui13 = self.data.read_int()\n            self.ui14 = self.data.read_int()\n\n            assert self.data.read_int() == 55\n\n        if self.game_version >= 31:\n            self.ub3 = self.data.read_bool()\n            self.item_reward_stages.read_item_obtains(self.data)\n            self.gatya.read_stepup(self.data)\n\n            self.backup_frame = self.data.read_int()\n\n            assert self.data.read_int() == 56\n\n        if self.game_version >= 32:\n            self.ub4 = self.data.read_bool()\n            self.cats.read_favorites(self.data)\n\n            assert self.data.read_int() == 57\n\n        if self.game_version >= 33:\n            self.dojo = core.Dojo.read_chapters(self.data)\n            self.dojo.read_item_locks(self.data)\n\n            assert self.data.read_int() == 58\n\n        if self.game_version >= 34:\n            self.last_checked_zombie_time = self.data.read_double()\n            self.outbreaks = core.Outbreaks.read_chapters(self.data)\n            self.outbreaks.read_2(self.data)\n            self.scheme_items = core.SchemeItems.read(self.data)\n\n        if self.game_version >= 35:\n            self.outbreaks.read_current_outbreaks(self.data, self.game_version)\n            self.first_locks = self.data.read_int_bool_dict()\n            self.energy_penalty_timestamp = self.data.read_double()\n\n            assert self.data.read_int() == 60\n\n        if self.game_version >= 36:\n            self.cats.read_chara_new_flags(self.data)\n            self.shown_maxcollab_mg = self.data.read_bool()\n            self.item_pack.read_displayed_packs(self.data)\n\n            assert self.data.read_int() == 61\n\n        if self.game_version >= 38:\n            self.unlock_popups = core.UnlockPopups.read(self.data)\n            assert self.data.read_int() == 63\n\n        if self.game_version >= 39:\n            self.ototo = core.Ototo.read(self.data)\n            self.ototo.read_2(self.data, self.game_version)\n            self.last_checked_castle_time = self.data.read_double()\n\n            assert self.data.read_int() == 64\n\n        if self.game_version >= 40:\n            self.beacon_base = core.BeaconEventListScene.read(self.data)\n\n            assert self.data.read_int() == 65\n\n        if self.game_version >= 41:\n            self.tower = core.TowerChapters.read(self.data)\n            self.missions = core.Missions.read(self.data, self.game_version)\n            self.tower.read_item_obtain_states(self.data)\n\n            assert self.data.read_int() == 66\n\n        if self.game_version >= 42:\n            self.dojo.read_ranking(self.data, self.game_version)\n            self.item_pack.read_three_days(self.data)\n            self.challenge = core.ChallengeChapters.read(self.data)\n            self.challenge.read_scores(self.data)\n            self.challenge.read_popup(self.data)\n\n            assert self.data.read_int() == 67\n\n        if self.game_version >= 43:\n            self.missions.read_weekly_missions(self.data)\n            self.dojo.ranking.read_did_win_rewards(self.data)\n            self.event_update_flags = self.data.read_bool()\n\n            assert self.data.read_int() == 68\n\n        if self.game_version >= 44:\n            self.event_stages.read_dicts(self.data)\n            self.cotc_1_complete = self.data.read_int()\n\n            assert self.data.read_int() == 69\n\n        if self.game_version >= 46:\n            self.gamatoto.read_collab_data(self.data)\n\n            assert self.data.read_int() == 71\n\n        if self.game_version < 90300:\n            self.map_resets = core.MapResets.read(self.data)\n\n            assert self.data.read_int() == 72\n\n        if self.game_version >= 51:\n            self.uncanny = core.UncannyChapters.read(self.data)\n            assert self.data.read_int() == 76\n\n        if self.game_version >= 77:\n            self.catamin_stages = core.UncannyChapters.read(self.data)\n\n            self.lucky_tickets = self.data.read_int_list()\n\n            self.ub5 = self.data.read_bool()\n\n            assert self.data.read_int() == 77\n\n        if self.game_version >= 80000:\n            self.officer_pass.read_gold_pass(self.data, self.game_version)\n            self.cats.read_talents(self.data)\n            self.np = self.data.read_int()\n            self.ub6 = self.data.read_bool()\n\n            assert self.data.read_int() == 80000\n\n        if self.game_version >= 80200:\n            self.ub7 = self.data.read_bool()\n            self.leadership = self.data.read_short()\n            self.officer_pass.read_cat_data(self.data)\n\n            assert self.data.read_int() == 80200\n\n        if self.game_version >= 80300:\n            self.filibuster_stage_id = self.data.read_byte()\n            self.filibuster_stage_enabled = self.data.read_bool()\n\n            assert self.data.read_int() == 80300\n\n        if self.game_version >= 80500:\n            self.stage_ids_10s = self.data.read_int_list()\n\n            assert self.data.read_int() == 80500\n\n        if self.game_version >= 80600:\n            length = self.data.read_short()\n            self.uil6 = self.data.read_int_list(length=length)\n            self.legend_quest = core.LegendQuestChapters.read(self.data)\n            self.ush1 = self.data.read_short()\n            self.uby1 = self.data.read_byte()\n\n            assert self.data.read_int() == 80600\n\n        if self.game_version >= 80700:\n            length = self.data.read_int()\n            self.uiid1: dict[int, list[int]] = {}\n            for _ in range(length):\n                key = self.data.read_int()\n                value = self.data.read_int_list()\n                self.uiid1[key] = value\n\n            assert self.data.read_int() == 80700\n\n        if self.game_version >= 100600:\n            if self.is_en():\n                self.uby2 = self.data.read_byte()\n                assert self.data.read_int() == 100600\n\n        if self.game_version >= 81000:\n            self.restart_pack = self.data.read_byte()\n            assert self.data.read_int() == 81000\n\n        if self.game_version >= 90000:\n            self.medals = core.Medals.read(self.data)\n            self.wildcat_slots = core.GamblingEvent.read(self.data, self.game_version)\n\n            assert self.data.read_int() == 90000\n\n        if self.game_version >= 90100:\n            self.ush2 = self.data.read_short()\n            self.ush3 = self.data.read_short()\n            self.ui15 = self.data.read_int()\n            self.ud1 = self.data.read_double()\n\n            assert self.data.read_int() == 90100\n\n        if self.game_version >= 90300:\n            length = self.data.read_short()\n            self.utl1: list[tuple[int, int, int, int, int, int, int]] = []\n            for _ in range(length):\n                i1 = self.data.read_int()\n                i2 = self.data.read_int()\n                i3 = self.data.read_short()\n                i4 = self.data.read_int()\n                i5 = self.data.read_int()\n                i6 = self.data.read_int()\n                i7 = self.data.read_short()\n                self.utl1.append((i1, i2, i3, i4, i5, i6, i7))\n\n            length = self.data.read_short()\n            self.uidd1 = self.data.read_int_double_dict(length)\n\n            self.gauntlets = core.GauntletChapters.read(self.data)\n\n            assert self.data.read_int() == 90300\n\n        if self.game_version >= 90400:\n            self.enigma_clears = core.GauntletChapters.read(self.data)\n            self.enigma = core.Enigma.read(self.data, self.game_version)\n            self.cleared_slots = core.ClearedSlots.read(self.data)\n\n            assert self.data.read_int() == 90400\n\n        if self.game_version >= 90500:\n            self.collab_gauntlets = core.GauntletChapters.read(self.data)\n            self.ub8 = self.data.read_bool()\n            self.ud2 = self.data.read_double()\n            self.ud3 = self.data.read_double()\n            self.ui16 = self.data.read_int()\n            if self.game_version >= 100300:\n                self.uby3 = self.data.read_byte()\n                self.ub9 = self.data.read_bool()\n                self.ud4 = self.data.read_double()\n                self.ud5 = self.data.read_double()\n\n            if self.game_version >= 130700:\n                length = self.data.read_short()\n                self.uiid3: dict[int, int] = {}\n                for _ in range(length):\n                    key = self.data.read_int()\n                    value = self.data.read_byte()\n                    self.uiid3[key] = value\n\n                length = self.data.read_short()\n                self.uidd2: dict[int, float] = {}\n                for _ in range(length):\n                    key = self.data.read_int()\n                    value = self.data.read_double()\n                    self.uidd2[key] = value\n\n            if self.game_version >= 140100:\n                length = self.data.read_short()\n                self.uidd3: dict[int, float] = {}\n                for _ in range(length):\n                    key = self.data.read_int()\n                    value = self.data.read_double()\n                    self.uidd3[key] = value\n\n            assert self.data.read_int() == 90500\n\n        if self.game_version >= 90700:\n            self.talent_orbs = core.TalentOrbs.read(self.data, self.game_version)\n            length = self.data.read_short()\n            self.uidiid2: dict[int, dict[int, int]] = {}\n            for _ in range(length):\n                key = self.data.read_short()\n                length = self.data.read_byte()\n                for _ in range(length):\n                    key2 = self.data.read_byte()\n                    value = self.data.read_short()\n                    if key not in self.uidiid2:\n                        self.uidiid2[key] = {}\n                    self.uidiid2[key][key2] = value\n                if length == 0:\n                    self.uidiid2[key] = {}\n\n            self.ub10 = self.data.read_bool()\n\n            assert self.data.read_int() == 90700\n\n        if self.game_version >= 90800:\n            length = self.data.read_short()\n            self.uil7 = self.data.read_int_list(length)\n            self.ubl2 = self.data.read_bool_list(10)\n\n            assert self.data.read_int() == 90800\n\n        if self.game_version >= 90900:\n            self.cat_shrine = core.CatShrine.read(self.data)\n            self.ud6 = self.data.read_double()\n            self.ud7 = self.data.read_double()\n\n            assert self.data.read_int() == 90900\n\n        if self.game_version >= 91000:\n            self.lineups.read_slot_names(self.data, self.game_version)\n\n            assert self.data.read_int() == 91000\n\n        if self.game_version >= 100000:\n            self.legend_tickets = self.data.read_int()\n            length = self.data.read_byte()\n            self.uiil1: list[tuple[int, int]] = []\n            for _ in range(length):\n                i1 = self.data.read_byte()\n                i2 = self.data.read_int()\n                self.uiil1.append((i1, i2))\n\n            self.ub11 = self.data.read_bool()\n            self.ub12 = self.data.read_bool()\n\n            self.password_refresh_token = self.data.read_string()\n\n            self.ub13 = self.data.read_bool()\n            self.uby4 = self.data.read_byte()\n            self.uby5 = self.data.read_byte()\n            self.ud8 = self.data.read_double()\n            self.ud9 = self.data.read_double()\n\n            assert self.data.read_int() == 100000\n\n        if self.game_version >= 100100:\n            self.date_int = self.data.read_int()\n\n            assert self.data.read_int() == 100100\n\n        if self.game_version >= 100300:\n            self.battle_items.read_endless_items(self.data)\n\n            assert self.data.read_int() == 100300\n\n        if self.game_version >= 100400:\n            length = self.data.read_byte()\n            self.event_capsules_2 = self.data.read_int_list(length)\n            self.two_battle_lines = self.data.read_bool()\n\n            assert self.data.read_int() == 100400\n\n        if self.game_version >= 100600:\n            self.ud10 = self.data.read_double()\n            self.platinum_shards = self.data.read_int()\n            self.ub15 = self.data.read_bool()\n\n            assert self.data.read_int() == 100600\n\n        if self.game_version >= 100700:\n            self.cat_scratcher = core.GamblingEvent.read(self.data, self.game_version)\n\n            assert self.data.read_int() == 100700\n\n        if self.game_version >= 100900:\n            self.aku = core.AkuChapters.read(self.data)\n            self.ub16 = self.data.read_bool()\n            self.ub17 = self.data.read_bool()\n\n            length = self.data.read_short()\n            self.ushdshd2: dict[int, list[int]] = {}\n            for _ in range(length):\n                key = self.data.read_short()\n                length = self.data.read_short()\n                for _ in range(length):\n                    value = self.data.read_short()\n                    if key not in self.ushdshd2:\n                        self.ushdshd2[key] = []\n                    self.ushdshd2[key].append(value)\n                if length == 0:\n                    self.ushdshd2[key] = []\n\n            length = self.data.read_short()\n            self.ushdd: dict[int, float] = {}\n            for _ in range(length):\n                key = self.data.read_short()\n                value = self.data.read_double()\n                self.ushdd[key] = value\n\n            length = self.data.read_short()\n            self.ushdd2: dict[int, float] = {}\n            for _ in range(length):\n                key = self.data.read_short()\n                value = self.data.read_double()\n                self.ushdd2[key] = value\n\n            self.ub18 = self.data.read_bool()\n\n            assert self.data.read_int() == 100900\n\n        if self.game_version >= 101000:\n            self.uby6 = self.data.read_byte()\n\n            assert self.data.read_int() == 101000\n\n        if self.game_version >= 110000:\n            length = self.data.read_short()\n            self.uidtii: dict[int, tuple[int, int]] = {}\n            for _ in range(length):\n                key = self.data.read_int()\n                value = (\n                    self.data.read_byte(),\n                    self.data.read_byte(),\n                )\n                self.uidtii[key] = value\n\n            assert self.data.read_int() == 110000\n\n        if self.game_version >= 110500:\n            self.behemoth_culling = core.GauntletChapters.read(self.data)\n            self.ub19 = self.data.read_bool()\n\n            assert self.data.read_int() == 110500\n\n        if self.game_version >= 110600:\n            self.ub20 = self.data.read_bool()\n\n            assert self.data.read_int() == 110600\n\n        if self.game_version >= 110700:\n            length = self.data.read_int()\n            self.uidtff: dict[int, tuple[float, float]] = {}\n            for _ in range(length):\n                key = self.data.read_int()\n                value = (\n                    self.data.read_double(),\n                    self.data.read_double(),\n                )\n                self.uidtff[key] = value\n\n            if self.not_jp():\n                self.ub20 = self.data.read_bool()\n\n            assert self.data.read_int() == 110700\n\n        if self.game_version >= 110800:\n            self.cat_shrine.read_dialogs(self.data)\n            self.ub21 = self.data.read_bool()\n            self.dojo_3x_speed = self.data.read_bool()\n            self.ub22 = self.data.read_bool()\n            self.ub23 = self.data.read_bool()\n\n            assert self.data.read_int() == 110800\n\n        if self.game_version >= 111000:\n            self.ui17 = self.data.read_int()\n            self.ush4 = self.data.read_short()\n            self.uby7 = self.data.read_byte()\n            self.uby8 = self.data.read_byte()\n            self.ub24 = self.data.read_bool()\n            self.uby9 = self.data.read_byte()\n\n            length = self.data.read_byte()\n            self.ushl1 = self.data.read_short_list(length)\n\n            length = self.data.read_short()\n            self.ushl2 = self.data.read_short_list(length)\n\n            length = self.data.read_short()\n            self.ushl3 = self.data.read_short_list(length)\n\n            self.ui18 = self.data.read_int()\n            self.ui19 = self.data.read_int()\n            self.ui20 = self.data.read_int()\n            self.ush5 = self.data.read_short()\n            self.ush6 = self.data.read_short()\n            self.ush7 = self.data.read_short()\n            self.ush8 = self.data.read_short()\n            self.uby10 = self.data.read_byte()\n            self.ub25 = self.data.read_bool()\n            self.ub26 = self.data.read_bool()\n            self.ub27 = self.data.read_bool()\n            self.ub28 = self.data.read_bool()\n            self.ub29 = self.data.read_bool()\n            self.ub30 = self.data.read_bool()\n            self.uby11 = self.data.read_byte()\n\n            length = self.data.read_short()\n            self.ushl4 = self.data.read_short_list(length)\n\n            self.ubl3 = self.data.read_bool_list(14)\n\n            length = self.data.read_byte()\n            self.labyrinth_medals = self.data.read_short_list(length)\n\n            assert self.data.read_int() == 111000\n\n        if self.game_version >= 120000:\n            self.zero_legends = core.ZeroLegendsChapters.read(self.data)\n            self.uby12 = self.data.read_byte()\n\n            assert self.data.read_int() == 120000\n\n        if self.game_version >= 120100:\n            length = self.data.read_short()\n            self.ushl6 = self.data.read_short_list(length)\n\n            assert self.data.read_int() == 120100\n\n        if self.game_version >= 120200:\n            self.ub31 = self.data.read_bool()\n            self.ush9 = self.data.read_short()\n            length = self.data.read_byte()\n            self.ushshd: dict[int, int] = {}\n            for _ in range(length):\n                key = self.data.read_short()\n                value = self.data.read_short()\n                self.ushshd[key] = value\n\n            assert self.data.read_int() == 120200\n\n        if self.game_version >= 120400:\n            self.ud11 = self.data.read_double()\n            self.ud12 = self.data.read_double()\n\n            assert self.data.read_int() == 120400\n\n        if self.game_version >= 120500:\n            self.ub32 = self.data.read_bool()\n            self.ub33 = self.data.read_bool()\n            self.ub34 = self.data.read_bool()\n\n            self.ui21 = self.data.read_int()\n            self.golden_cpu_count = self.data.read_byte()\n\n            assert self.data.read_int() == 120500\n\n        if self.game_version >= 120600:\n            self.sound_effects_volume = self.data.read_byte()\n            self.background_music_volume = self.data.read_byte()\n\n            assert self.data.read_int() == 120600\n\n        if (self.not_jp() and self.game_version >= 120700) or (\n            self.is_jp() and self.game_version >= 130000\n        ):\n            length = self.data.read_byte()\n            self.ustl1: list[tuple[str, str]] = []\n            for _ in range(length):\n                s1 = self.data.read_string()\n                s2 = self.data.read_string()\n                self.ustl1.append((s1, s2))\n\n            if self.not_jp():\n                assert self.data.read_int() == 120700\n            else:\n                assert self.data.read_int() == 130000\n\n        if self.game_version >= 130100:\n            length = self.data.read_int()\n            self.utl3: list[tuple[int, int]] = []\n            for _ in range(length):\n                i1 = self.data.read_int()\n                i2 = self.data.read_long()\n                self.utl3.append((i1, i2))\n\n            assert self.data.read_int() == 130100\n\n        if self.game_version >= 130301:\n            length = self.data.read_int()\n            self.ustid1: dict[str, tuple[int, float]] = {}\n            for _ in range(length):\n                key = self.data.read_string()\n                value_1 = self.data.read_int()\n                value_2 = self.data.read_double()\n                self.ustid1[key] = (value_1, value_2)\n\n            assert self.data.read_int() == 130301\n\n        if self.game_version >= 130400:\n            self.ud13 = self.data.read_double()\n            self.ud14 = self.data.read_double()\n\n            assert self.data.read_int() == 130400\n\n        if self.game_version >= 130500:\n            self.utl4: list[tuple[int, list[tuple[int, int, int, list[int]]]]] = []\n            length1 = self.data.read_short()\n            for _ in range(length1):\n                id = self.data.read_byte()\n                length2 = self.data.read_byte()\n                ls2: list[tuple[int, int, int, list[int]]] = []\n                for _ in range(length2):\n                    v1 = self.data.read_byte()\n                    v2 = self.data.read_byte()\n                    v3 = self.data.read_byte()\n\n                    length3 = self.data.read_short()\n                    ls1: list[int] = []\n\n                    for _ in range(length3):\n                        val = self.data.read_short()\n                        ls1.append(val)\n\n                    ls2.append((v1, v2, v3, ls1))\n\n                self.utl4.append((id, ls2))\n\n            assert self.data.read_int() == 130500\n\n        if self.game_version >= 130600:\n            self.uby14 = self.data.read_byte()\n\n            if self.not_jp():\n                self.ush12 = self.data.read_short()\n\n            assert self.data.read_int() == 130600\n\n        if self.game_version >= 130700:\n            if self.is_jp():\n                self.ush12 = self.data.read_short()\n            self.ud15 = self.data.read_double()\n            self.uby15 = self.data.read_byte()\n            self.uby16 = self.data.read_byte()\n\n            self.ush11 = self.data.read_short()\n            self.uby17 = self.data.read_byte()\n            self.uby18 = self.data.read_byte()\n            self.uby19 = self.data.read_byte()\n\n            self.ud16 = self.data.read_double()\n\n            length1 = self.data.read_short()\n\n            self.ushd1: dict[int, tuple[int, int, dict[int, int]]] = {}\n\n            for _ in range(length1):\n                key = self.data.read_short()\n                value = self.data.read_short()\n                value_2 = self.data.read_int()\n\n                length2 = self.data.read_short()\n\n                data2: dict[int, int] = {}\n\n                for _ in range(length2):\n                    key2 = self.data.read_short()\n                    value3 = self.data.read_short()\n\n                    data2[key2] = value3\n\n                self.ushd1[key] = (value, value_2, data2)\n\n            assert self.data.read_int() == 130700\n\n        if self.game_version >= 140000:\n            self.ui22 = self.data.read_int()\n            self.ud17 = self.data.read_double()\n            self.uby20 = self.data.read_byte()\n\n            length = self.data.read_byte()\n\n            self.uild1: dict[int, list[int]] = {}\n\n            for _ in range(length):\n                key = self.data.read_int()\n                length2 = self.data.read_byte()\n                data3: list[int] = []\n                for _ in range(length2):\n                    value = self.data.read_byte()\n                    data3.append(value)\n\n                self.uild1[key] = data3\n\n            self.dojo_chapters = core.ZeroLegendsChapters.read(self.data)\n\n            length = self.data.read_short()\n\n            self.uil9: list[int] = []\n            for _ in range(length):\n                self.uil9.append(self.data.read_int())\n\n            self.ub35 = self.data.read_bool()\n            self.ud18 = self.data.read_double()\n\n            length = self.data.read_short()\n\n            self.ushd2: dict[int, int] = {}\n\n            for _ in range(length):\n                key = self.data.read_short()\n                value = self.data.read_byte()\n                self.ushd2[key] = value\n\n            assert self.data.read_int() == 140000\n\n        if self.game_version >= 140100 and self.game_version < 140500:\n            self.uby21 = self.data.read_byte()\n            assert self.data.read_int() == 140100\n\n        if self.game_version >= 140200:\n            length = self.data.read_byte()\n\n            self.uil10: list[\n                tuple[\n                    int,\n                    int,\n                    bool,\n                    bool,\n                    bool,\n                    int,\n                    int,\n                    int,\n                    bool,\n                    bool,\n                    bool,\n                    str | None,\n                    bool,\n                ]\n            ] = []\n\n            for _ in range(length):\n                val_1 = self.data.read_int()\n                val_2 = self.data.read_int()\n                val_3 = self.data.read_bool()\n                val_4 = self.data.read_bool()\n                val_5 = self.data.read_bool()\n                val_6 = self.data.read_int()\n                val_7 = self.data.read_int()\n                val_8 = self.data.read_int()\n                val_9 = self.data.read_bool()\n                val_10 = self.data.read_bool()\n                val_11 = self.data.read_bool()\n\n                val_12 = None\n\n                if self.game_version >= 140500:\n                    # game seems to read more than just this, may break in the future\n                    val_12 = self.data.read_string()\n\n                val_13 = self.data.read_bool()\n\n                self.uil10.append(\n                    (\n                        val_1,\n                        val_2,\n                        val_3,\n                        val_4,\n                        val_5,\n                        val_6,\n                        val_7,\n                        val_8,\n                        val_9,\n                        val_10,\n                        val_11,\n                        val_12,\n                        val_13,\n                    )\n                )\n\n            length = self.data.read_byte()\n\n            self.uid1: dict[int, float] = {}\n\n            for _ in range(length):\n                key = self.data.read_int()\n                value = self.data.read_double()\n\n                self.uid1[key] = value\n\n            self.hundred_million_ticket = self.data.read_int()\n\n            assert self.data.read_int() == 140200\n\n        if self.game_version >= 140300:\n            length = self.data.read_byte()\n            self.uil11: list[int] = []\n            for _ in range(length):\n                val = self.data.read_byte()\n                self.uil11.append(val)\n\n            if self.game_version >= 150300:\n                self.ui24 = self.data.read_int()\n                self.ub38 = self.data.read_bool()\n\n            self.ub36 = self.data.read_bool()\n\n            length = self.data.read_byte()\n\n            self.treasure_chests: list[int] = []\n\n            for _ in range(length):\n                value = self.data.read_int()\n                self.treasure_chests.append(value)\n\n            self.ui23 = self.data.read_int()\n            length = self.data.read_short()\n\n            self.uil13: list[int] = []\n\n            for _ in range(length):\n                self.uil13.append(self.data.read_int())\n\n            self.ub37 = self.data.read_bool()\n\n            assert self.data.read_int() == 140300\n\n        self.remaining_data = self.data.read_to_end(32)\n\n    def save(self, data: core.Data):\n        self.data = data\n        self.dst_index = 0\n        self.data.clear()\n        self.data.enable_buffer()\n\n        self.data.write_int(self.game_version.game_version)\n\n        if self.game_version >= 10 or self.not_jp():\n            self.data.write_bool(self.ub1)\n\n        self.data.write_bool(self.mute_bgm)\n        self.data.write_bool(self.mute_se)\n\n        self.data.write_int(self.catfood)\n        self.data.write_int(self.current_energy)\n\n        self.data.write_int(self.date.year)\n        self.data.write_int(self.year)\n\n        self.data.write_int(self.date.month)\n        self.data.write_int(self.month)\n\n        self.data.write_int(self.date.day)\n        self.data.write_int(self.day)\n\n        self.data.write_double(self.timestamp)\n\n        self.data.write_int(self.date.hour)\n        self.data.write_int(self.date.minute)\n        self.data.write_int(self.date.second)\n\n        self.write_dst()\n\n        self.data.write_int(self.ui1)\n        self.data.write_int(self.stamp_value_save)\n        self.data.write_int(self.ui2)\n\n        self.data.write_int(self.upgrade_state)\n\n        self.data.write_int(self.xp)\n        self.data.write_int(self.tutorial_state)\n\n        self.data.write_int(self.ui3)\n        self.data.write_int(self.koreaSuperiorTreasureState)\n\n        self.data.write_int_list(self.unlock_popups_11, write_length=False, length=3)\n        self.data.write_int(self.ui5)\n        self.data.write_int(self.unlock_enemy_guide)\n        self.data.write_int(self.ui6)\n        self.data.write_bool(self.ub0)\n        self.data.write_int(self.ui7)\n        self.data.write_int(self.cleared_eoc_1)\n        self.data.write_int(self.ui8)\n        self.data.write_int(self.unlocked_ending)\n\n        self.lineups.write(self.data, self.game_version)\n\n        self.stamp_data.write(self.data)\n\n        self.story.write(self.data)\n\n        if 20 <= self.game_version and self.game_version <= 25:\n            self.data.write_int_list(self.enemy_guide, write_length=False, length=231)\n        else:\n            self.data.write_int_list(self.enemy_guide)\n\n        self.cats.write_unlocked(self.data, self.game_version)\n        self.cats.write_upgrade(self.data, self.game_version)\n        self.cats.write_current_form(self.data, self.game_version)\n\n        self.special_skills.write_upgrades(self.data)\n\n        if self.game_version <= 25:\n            self.data.write_int_list(self.menu_unlocks, write_length=False, length=5)\n            self.data.write_int_list(self.unlock_popups_0, write_length=False, length=5)\n        elif self.game_version <= 26:\n            self.data.write_int_list(self.menu_unlocks, write_length=False, length=6)\n            self.data.write_int_list(self.unlock_popups_0, write_length=False, length=6)\n        else:\n            self.data.write_int_list(self.menu_unlocks)\n            self.data.write_int_list(self.unlock_popups_0)\n\n        self.battle_items.write_items(self.data)\n\n        if self.game_version <= 26:\n            self.data.write_int_list(self.new_dialogs_2, write_length=False, length=17)\n        else:\n            self.data.write_int_list(self.new_dialogs_2)\n\n        self.data.write_int_list(self.uil1, write_length=False, length=20)\n        self.data.write_int_list(self.moneko_bonus, write_length=False, length=1)\n        self.data.write_int_list(\n            self.daily_reward_initialized, write_length=False, length=1\n        )\n\n        self.battle_items.write_locked_items(self.data)\n\n        self.write_dst()\n        self.data.write_date(self.date_2)\n\n        self.story.write_treasure_festival(self.data)\n\n        self.write_dst()\n        self.data.write_date(self.date_3)\n\n        if self.game_version <= 37:\n            self.data.write_int(self.ui0)\n\n        self.data.write_int(self.stage_unlock_cat_value)\n        self.data.write_int(self.show_ending_value)\n        self.data.write_int(self.chapter_clear_cat_unlock)\n        self.data.write_int(self.ui9)\n        self.data.write_int(self.ios_android_month)\n        self.data.write_int(self.ui10)\n        self.data.write_string(self.save_data_4_hash)\n\n        self.mysale.write_bonus_hash(self.data)\n        self.data.write_int_list(self.chara_flags, write_length=False, length=2)\n\n        if self.game_version <= 37:\n            self.data.write_int(self.uim1)\n            self.data.write_bool(self.ubm1)\n\n        self.data.write_int_list(self.chara_flags_2, write_length=False, length=2)\n\n        self.data.write_int(self.normal_tickets)\n        self.data.write_int(self.rare_tickets)\n\n        self.cats.write_gatya_seen(self.data, self.game_version)\n        self.special_skills.write_gatya_seen(self.data)\n        self.cats.write_storage(self.data, self.game_version)\n\n        self.event_stages.write(self.data, self.game_version)\n\n        self.data.write_int(self.itf1_ending)\n        self.data.write_int(self.continue_flag)\n\n        if 20 <= self.game_version:\n            self.data.write_int_list(\n                self.unlock_popups_8, write_length=False, length=36\n            )\n\n        if 20 <= self.game_version and self.game_version <= 25:\n            self.data.write_int_list(self.unit_drops, write_length=False, length=110)\n        elif 26 <= self.game_version:\n            self.data.write_int_list(self.unit_drops)\n\n        self.gatya.write_rare_normal_seed(self.data)\n\n        self.data.write_bool(self.get_event_data)\n        self.data.write_bool_list(self.achievements, write_length=False, length=7)\n\n        self.data.write_int(self.os_value)\n\n        self.write_dst()\n        self.data.write_date(self.date_4)\n\n        self.gatya.write2(self.data)\n\n        if self.not_jp():\n            self.data.write_string(self.player_id)\n\n        self.data.write_string_list(self.order_ids)\n\n        if self.not_jp():\n            self.data.write_double(self.g_timestamp)\n            self.data.write_double(self.g_servertimestamp)\n            self.data.write_double(self.m_gettimesave)\n            self.data.write_string_list(self.usl1)\n            self.data.write_bool(self.energy_notification)\n            self.data.write_int(self.full_gameversion)\n\n        self.lineups.write_2(self.data, self.game_version)\n        self.event_stages.write_legend_restrictions(self.data, self.game_version)\n\n        if self.game_version <= 37:\n            self.data.write_int_list(self.uil2, write_length=False, length=7)\n            self.data.write_int_list(self.uil3, write_length=False, length=7)\n            self.data.write_int_list(self.uil4, write_length=False, length=7)\n\n        self.data.write_double(self.g_timestamp_2)\n        self.data.write_double(self.g_servertimestamp_2)\n        self.data.write_double(self.m_gettimesave_2)\n        self.data.write_double(self.unknown_timestamp)\n\n        self.gatya.write_trade_progress(self.data)\n\n        if self.game_version <= 37:\n            self.data.write_string_list(self.usl2)\n\n        if self.not_jp():\n            self.data.write_double(self.m_dGetTimeSave2)\n        else:\n            self.data.write_int(self.ui11)\n\n        if 20 <= self.game_version and self.game_version <= 25:\n            self.data.write_bool_list(self.ubl1, write_length=False, length=12)\n        elif 26 <= self.game_version and self.game_version < 39:\n            self.data.write_bool_list(self.ubl1)\n\n        self.cats.write_max_upgrade_levels(self.data, self.game_version)\n        self.special_skills.write_max_upgrade_levels(self.data)\n\n        self.user_rank_rewards.write(self.data, self.game_version)\n\n        if self.is_jp():\n            self.data.write_double(self.m_dGetTimeSave2)\n\n        self.cats.write_unlocked_forms(self.data, self.game_version)\n\n        self.data.write_string(self.transfer_code)\n        self.data.write_string(self.confirmation_code)\n        self.data.write_bool(self.transfer_flag)\n\n        if 20 <= self.game_version:\n            self.item_reward_stages.write(self.data, self.game_version)\n            self.timed_score_stages.write(self.data, self.game_version)\n\n            self.data.write_string(self.inquiry_code)\n            self.officer_pass.write(self.data)\n            self.data.write_byte(self.has_account)\n            self.data.write_int(self.backup_state)\n\n            if self.not_jp():\n                self.data.write_bool(self.ub2)\n\n            self.data.write_int(44)\n            self.data.write_int(self.itf1_complete)\n            self.story.write_itf_timed_scores(self.data)\n            self.data.write_int(self.title_chapter_bg)\n\n            if self.game_version > 26:\n                self.data.write_int_list(self.combo_unlocks)\n\n            self.data.write_bool(self.combo_unlocked_10k_ur)\n\n            self.data.write_int(45)\n\n        if 21 <= self.game_version:\n            self.data.write_int(46)\n            self.gatya.write_event_seed(self.data)\n            if self.game_version < 34:\n                self.data.write_int_list(\n                    self.event_capsules, write_length=False, length=100\n                )\n                self.data.write_int_list(\n                    self.event_capsules_counter, write_length=False, length=100\n                )\n            else:\n                self.data.write_int_list(self.event_capsules)\n                self.data.write_int_list(self.event_capsules_counter)\n\n            self.data.write_int(47)\n\n        if 22 <= self.game_version:\n            self.data.write_int(48)\n\n        if 23 <= self.game_version:\n            if self.is_jp():\n                self.data.write_bool(self.energy_notification)\n\n            self.data.write_double(self.m_dGetTimeSave3)\n            if self.game_version < 26:\n                self.data.write_int_list(\n                    self.gatya_seen_lucky_drops,\n                    write_length=False,\n                    length=44,\n                )\n            else:\n                self.data.write_int_list(self.gatya_seen_lucky_drops)\n            self.data.write_bool(self.show_ban_message)\n            self.data.write_bool_list(\n                self.catfood_beginner_purchased,\n                write_length=False,\n                length=3,\n            )\n            self.data.write_double(self.next_week_timestamp)\n            self.data.write_bool_list(\n                self.catfood_beginner_expired, write_length=False, length=3\n            )\n            self.data.write_int(self.rank_up_sale_value)\n            self.data.write_int(49)\n\n        if 24 <= self.game_version:\n            self.data.write_int(50)\n\n        if 25 <= self.game_version:\n            self.data.write_int(51)\n\n        if 26 <= self.game_version:\n            self.cats.write_catguide_collected(self.data)\n            self.data.write_int(52)\n\n        if 27 <= self.game_version:\n            self.data.write_double(self.time_since_time_check_cumulative)\n            self.data.write_double(self.server_timestamp)\n            self.data.write_double(self.last_checked_energy_recovery_time)\n            self.data.write_double(self.time_since_check)\n            self.data.write_double(self.last_checked_expedition_time)\n\n            self.data.write_int_list(self.catfruit)\n            self.cats.write_fourth_forms(self.data)\n            self.cats.write_catseyes_used(self.data)\n            self.data.write_int_list(self.catseyes)\n            self.data.write_int_list(self.catamins)\n            self.gamatoto.write(self.data)\n\n            self.data.write_bool_list(self.unlock_popups_6)\n            self.ex_stages.write(self.data)\n\n            self.data.write_int(53)\n\n        if 29 <= self.game_version:\n            self.gamatoto.write_2(self.data)\n            self.data.write_int(54)\n            self.item_pack.write(self.data)\n            self.data.write_int(54)\n\n        if self.game_version >= 30:\n            self.gamatoto.write_skin(self.data)\n            self.data.write_int(self.platinum_tickets)\n            self.logins.write(self.data, self.game_version)\n\n            if self.game_version < 101000:\n                self.data.write_bool_list(self.reset_item_reward_flags)\n\n            self.data.write_double(self.reward_remaining_time)\n            self.data.write_double(self.last_checked_reward_time)\n\n            self.data.write_int_tuple_list(\n                self.announcements, write_length=False, length=16\n            )\n            self.data.write_int(self.backup_counter)\n            self.data.write_int(self.ui12)\n            self.data.write_int(self.ui13)\n            self.data.write_int(self.ui13)\n            self.data.write_int(55)\n\n        if self.game_version >= 31:\n            self.data.write_bool(self.ub3)\n            self.item_reward_stages.write_item_obtains(self.data)\n            self.gatya.write_stepup(self.data)\n\n            self.data.write_int(self.backup_frame)\n            self.data.write_int(56)\n\n        if self.game_version >= 32:\n            self.data.write_bool(self.ub4)\n            self.cats.write_favorites(self.data)\n            self.data.write_int(57)\n\n        if self.game_version >= 33:\n            self.dojo.write_chapters(self.data)\n            self.dojo.write_item_locks(self.data)\n            self.data.write_int(58)\n\n        if self.game_version >= 34:\n            self.data.write_double(self.last_checked_zombie_time)\n            self.outbreaks.write_chapters(self.data)\n            self.outbreaks.write_2(self.data)\n            self.scheme_items.write(self.data)\n\n        if self.game_version >= 35:\n            self.outbreaks.write_current_outbreaks(self.data, self.game_version)\n            self.data.write_int_bool_dict(self.first_locks)\n            self.data.write_double(self.energy_penalty_timestamp)\n            self.data.write_int(60)\n\n        if self.game_version >= 36:\n            self.cats.write_chara_new_flags(self.data)\n            self.data.write_bool(self.shown_maxcollab_mg)\n            self.item_pack.write_displayed_packs(self.data)\n            self.data.write_int(61)\n\n        if self.game_version >= 38:\n            self.unlock_popups.write(self.data)\n            self.data.write_int(63)\n\n        if self.game_version >= 39:\n            self.ototo.write(self.data)\n            self.ototo.write_2(self.data, self.game_version)\n            self.data.write_double(self.last_checked_castle_time)\n            self.data.write_int(64)\n\n        if self.game_version >= 40:\n            self.beacon_base.write(self.data)\n            self.data.write_int(65)\n\n        if self.game_version >= 41:\n            self.tower.write(self.data)\n            self.missions.write(self.data, self.game_version)\n            self.tower.write_item_obtain_states(self.data)\n            self.data.write_int(66)\n\n        if self.game_version >= 42:\n            self.dojo.write_ranking(self.data, self.game_version)\n            self.item_pack.write_three_days(self.data)\n            self.challenge.write(self.data)\n            self.challenge.write_scores(self.data)\n            self.challenge.write_popup(self.data)\n            self.data.write_int(67)\n\n        if self.game_version >= 43:\n            self.missions.write_weekly_missions(self.data)\n            self.dojo.ranking.write_did_win_rewards(self.data)\n            self.data.write_bool(self.event_update_flags)\n            self.data.write_int(68)\n\n        if self.game_version >= 44:\n            self.event_stages.write_dicts(self.data)\n            self.data.write_int(self.cotc_1_complete)\n            self.data.write_int(69)\n\n        if self.game_version >= 46:\n            self.gamatoto.write_collab_data(self.data)\n            self.data.write_int(71)\n\n        if self.game_version < 90300:\n            self.map_resets.write(self.data)\n            self.data.write_int(72)\n\n        if self.game_version >= 51:\n            self.uncanny.write(self.data)\n            self.data.write_int(76)\n\n        if self.game_version >= 77:\n            self.catamin_stages.write(self.data)\n            self.data.write_int_list(self.lucky_tickets)\n            self.data.write_bool(self.ub5)\n            self.data.write_int(77)\n\n        if self.game_version >= 80000:\n            self.officer_pass.write_gold_pass(self.data, self.game_version)\n            self.cats.write_talents(self.data)\n            self.data.write_int(self.np)\n            self.data.write_bool(self.ub6)\n            self.data.write_int(80000)\n\n        if self.game_version >= 80200:\n            self.data.write_bool(self.ub7)\n            self.data.write_short(self.leadership)\n            self.officer_pass.write_cat_data(self.data)\n            self.data.write_int(80200)\n\n        if self.game_version >= 80300:\n            self.data.write_byte(self.filibuster_stage_id)\n            self.data.write_bool(self.filibuster_stage_enabled)\n            self.data.write_int(80300)\n\n        if self.game_version >= 80500:\n            self.data.write_int_list(self.stage_ids_10s)\n            self.data.write_int(80500)\n\n        if self.game_version >= 80600:\n            self.data.write_short(len(self.uil6))\n            self.data.write_int_list(self.uil6, write_length=False)\n            self.legend_quest.write(self.data)\n            self.data.write_short(self.ush1)\n            self.data.write_byte(self.uby1)\n            self.data.write_int(80600)\n\n        if self.game_version >= 80700:\n            self.data.write_int(len(self.uiid1))\n            for key, value in self.uiid1.items():\n                self.data.write_int(key)\n                self.data.write_int_list(value)\n            self.data.write_int(80700)\n\n        if self.game_version >= 100600:\n            if self.is_en():\n                self.data.write_byte(self.uby2)\n                self.data.write_int(100600)\n\n        if self.game_version >= 81000:\n            self.data.write_byte(self.restart_pack)\n            self.data.write_int(81000)\n\n        if self.game_version >= 90000:\n            self.medals.write(self.data)\n            self.wildcat_slots.write(self.data, self.game_version)\n\n            self.data.write_int(90000)\n\n        if self.game_version >= 90100:\n            self.data.write_short(self.ush2)\n            self.data.write_short(self.ush3)\n            self.data.write_int(self.ui15)\n            self.data.write_double(self.ud1)\n            self.data.write_int(90100)\n\n        if self.game_version >= 90300:\n            self.data.write_short(len(self.utl1))\n            for tuple_ in self.utl1:\n                tuple_len = len(tuple_)\n                i1, i2, i3, i4, i5, i6, i7 = 0, 0, 0, 0, 0, 0, 0\n                if tuple_len >= 1:\n                    i1 = tuple_[0]\n                if tuple_len >= 2:\n                    i2 = tuple_[1]\n                if tuple_len >= 3:\n                    i3 = tuple_[2]\n                if tuple_len >= 4:\n                    i4 = tuple_[3]\n                if tuple_len >= 5:\n                    i5 = tuple_[4]\n                if tuple_len >= 6:\n                    i6 = tuple_[5]\n                if tuple_len >= 7:\n                    i7 = tuple_[6]\n\n                self.data.write_int(i1)\n                self.data.write_int(i2)\n                self.data.write_short(i3)\n                self.data.write_int(i4)\n                self.data.write_int(i5)\n                self.data.write_int(i6)\n                self.data.write_short(i7)\n\n            self.data.write_short(len(self.uidd1))\n            self.data.write_int_double_dict(self.uidd1, write_length=False)\n            self.gauntlets.write(self.data)\n            self.data.write_int(90300)\n\n        if self.game_version >= 90400:\n            self.enigma_clears.write(self.data)\n            self.enigma.write(self.data, self.game_version)\n            self.cleared_slots.write(self.data)\n            self.data.write_int(90400)\n\n        if self.game_version >= 90500:\n            self.collab_gauntlets.write(self.data)\n            self.data.write_bool(self.ub8)\n            self.data.write_double(self.ud2)\n            self.data.write_double(self.ud3)\n            self.data.write_int(self.ui16)\n            if self.game_version >= 100300:\n                self.data.write_byte(self.uby3)\n                self.data.write_bool(self.ub9)\n                self.data.write_double(self.ud4)\n                self.data.write_double(self.ud5)\n\n            if self.game_version >= 130700:\n                self.data.write_short(len(self.uiid3))\n                for key, value in self.uiid3.items():\n                    self.data.write_int(key)\n                    self.data.write_byte(value)\n\n                self.data.write_short(len(self.uidd2))\n                for key, value in self.uidd2.items():\n                    self.data.write_int(key)\n                    self.data.write_double(value)\n\n            if self.game_version >= 140100:\n                self.data.write_short(len(self.uidd3))\n                for key, value in self.uidd3.items():\n                    self.data.write_int(key)\n                    self.data.write_double(value)\n\n            self.data.write_int(90500)\n\n        if self.game_version >= 90700:\n            self.talent_orbs.write(self.data, self.game_version)\n            self.data.write_short(len(self.uidiid2))\n            for key, value in self.uidiid2.items():\n                self.data.write_short(key)\n                self.data.write_byte(len(value))\n                for key2, value2 in value.items():\n                    self.data.write_byte(key2)\n                    self.data.write_short(value2)\n\n            self.data.write_bool(self.ub10)\n            self.data.write_int(90700)\n\n        if self.game_version >= 90800:\n            self.data.write_short(len(self.uil7))\n            self.data.write_int_list(self.uil7, write_length=False)\n            self.data.write_bool_list(self.ubl2, write_length=False, length=10)\n            self.data.write_int(90800)\n\n        if self.game_version >= 90900:\n            self.cat_shrine.write(self.data)\n            self.data.write_double(self.ud6)\n            self.data.write_double(self.ud7)\n            self.data.write_int(90900)\n\n        if self.game_version >= 91000:\n            self.lineups.write_slot_names(self.data, self.game_version)\n            self.data.write_int(91000)\n\n        if self.game_version >= 100000:\n            self.data.write_int(self.legend_tickets)\n            self.data.write_byte(len(self.uiil1))\n            for key, value in self.uiil1:\n                self.data.write_byte(key)\n                self.data.write_int(value)\n\n            self.data.write_bool(self.ub11)\n            self.data.write_bool(self.ub12)\n\n            self.data.write_string(self.password_refresh_token)\n\n            self.data.write_bool(self.ub13)\n            self.data.write_byte(self.uby4)\n            self.data.write_byte(self.uby5)\n            self.data.write_double(self.ud8)\n            self.data.write_double(self.ud9)\n\n            self.data.write_int(100000)\n\n        if self.game_version >= 100100:\n            self.data.write_int(self.date_int)\n            self.data.write_int(100100)\n\n        if self.game_version >= 100300:\n            self.battle_items.write_endless_items(self.data)\n\n            self.data.write_int(100300)\n\n        if self.game_version >= 100400:\n            self.data.write_byte(len(self.event_capsules_2))\n            self.data.write_int_list(self.event_capsules_2, write_length=False)\n            self.data.write_bool(self.two_battle_lines)\n            self.data.write_int(100400)\n\n        if self.game_version >= 100600:\n            self.data.write_double(self.ud10)\n            self.data.write_int(self.platinum_shards)\n            self.data.write_bool(self.ub15)\n            self.data.write_int(100600)\n\n        if self.game_version >= 100700:\n            self.cat_scratcher.write(self.data, self.game_version)\n\n            self.data.write_int(100700)\n\n        if self.game_version >= 100900:\n            self.aku.write(self.data)\n            self.data.write_bool(self.ub16)\n            self.data.write_bool(self.ub17)\n\n            self.data.write_short(len(self.ushdshd2))\n            for key, value in self.ushdshd2.items():\n                self.data.write_short(key)\n                self.data.write_short(len(value))\n                for item in value:\n                    self.data.write_short(item)\n\n            self.data.write_short(len(self.ushdd))\n            for key, value in self.ushdd.items():\n                self.data.write_short(key)\n                self.data.write_double(value)\n\n            self.data.write_short(len(self.ushdd2))\n            for key, value in self.ushdd2.items():\n                self.data.write_short(key)\n                self.data.write_double(value)\n\n            self.data.write_bool(self.ub18)\n            self.data.write_int(100900)\n\n        if self.game_version >= 101000:\n            self.data.write_byte(self.uby6)\n            self.data.write_int(101000)\n\n        if self.game_version >= 110000:\n            self.data.write_short(len(self.uidtii))\n            for key, value in self.uidtii.items():\n                self.data.write_int(key)\n                self.data.write_byte(value[0])\n                self.data.write_byte(value[1])\n\n            self.data.write_int(110000)\n\n        if self.game_version >= 110500:\n            self.behemoth_culling.write(self.data)\n            self.data.write_bool(self.ub19)\n            self.data.write_int(110500)\n\n        if self.game_version >= 110600:\n            self.data.write_bool(self.ub20)\n            self.data.write_int(110600)\n\n        if self.game_version >= 110700:\n            self.data.write_int(len(self.uidtff))\n            for key, value in self.uidtff.items():\n                self.data.write_int(key)\n                self.data.write_double(value[0])\n                self.data.write_double(value[1])\n\n            if self.not_jp():\n                self.data.write_bool(self.ub20)\n            self.data.write_int(110700)\n\n        if self.game_version >= 110800:\n            self.cat_shrine.write_dialogs(self.data)\n            self.data.write_bool(self.ub21)\n            self.data.write_bool(self.dojo_3x_speed)\n            self.data.write_bool(self.ub22)\n            self.data.write_bool(self.ub23)\n\n            self.data.write_int(110800)\n\n        if self.game_version >= 111000:\n            self.data.write_int(self.ui17)\n            self.data.write_short(self.ush4)\n            self.data.write_byte(self.uby7)\n            self.data.write_byte(self.uby8)\n            self.data.write_bool(self.ub24)\n            self.data.write_byte(self.uby9)\n\n            self.data.write_byte(len(self.ushl1))\n            self.data.write_short_list(self.ushl1, write_length=False)\n\n            self.data.write_short(len(self.ushl2))\n            self.data.write_short_list(self.ushl2, write_length=False)\n\n            self.data.write_short(len(self.ushl3))\n            self.data.write_short_list(self.ushl3, write_length=False)\n\n            self.data.write_int(self.ui18)\n            self.data.write_int(self.ui19)\n            self.data.write_int(self.ui20)\n            self.data.write_short(self.ush5)\n            self.data.write_short(self.ush6)\n            self.data.write_short(self.ush7)\n            self.data.write_short(self.ush8)\n            self.data.write_byte(self.uby10)\n            self.data.write_bool(self.ub25)\n            self.data.write_bool(self.ub26)\n            self.data.write_bool(self.ub27)\n            self.data.write_bool(self.ub28)\n            self.data.write_bool(self.ub29)\n            self.data.write_bool(self.ub30)\n            self.data.write_byte(self.uby11)\n\n            self.data.write_short(len(self.ushl4))\n            self.data.write_short_list(self.ushl4, write_length=False)\n\n            self.data.write_bool_list(self.ubl3, write_length=False, length=14)\n\n            self.data.write_byte(len(self.labyrinth_medals))\n            self.data.write_short_list(self.labyrinth_medals, write_length=False)\n\n            self.data.write_int(111000)\n\n        if self.game_version >= 120000:\n            self.zero_legends.write(self.data)\n            self.data.write_byte(self.uby12)\n\n            self.data.write_int(120000)\n\n        if self.game_version >= 120100:\n            self.data.write_short(len(self.ushl6))\n            self.data.write_short_list(self.ushl6, write_length=False)\n\n            self.data.write_int(120100)\n\n        if self.game_version >= 120200:\n            self.data.write_bool(self.ub31)\n            self.data.write_short(self.ush9)\n            self.data.write_byte(len(self.ushshd))\n            for key, value in self.ushshd.items():\n                self.data.write_short(key)\n                self.data.write_short(value)\n\n            self.data.write_int(120200)\n\n        if self.game_version >= 120400:\n            self.data.write_double(self.ud11)\n            self.data.write_double(self.ud12)\n\n            self.data.write_int(120400)\n\n        if self.game_version >= 120500:\n            self.data.write_bool(self.ub32)\n            self.data.write_bool(self.ub33)\n            self.data.write_bool(self.ub34)\n            self.data.write_int(self.ui21)\n            self.data.write_byte(self.golden_cpu_count)\n\n            self.data.write_int(120500)\n\n        if self.game_version >= 120600:\n            self.data.write_byte(self.sound_effects_volume)\n            self.data.write_byte(self.background_music_volume)\n\n            self.data.write_int(120600)\n\n        if (self.not_jp() and self.game_version >= 120700) or (\n            self.is_jp() and self.game_version >= 130000\n        ):\n            self.data.write_byte(len(self.ustl1))\n            for str1, str2 in self.ustl1:\n                self.data.write_string(str1)\n                self.data.write_string(str2)\n\n            if self.not_jp():\n                self.data.write_int(120700)\n            else:\n                self.data.write_int(130000)\n\n        if self.game_version >= 130100:\n            self.data.write_int(len(self.utl3))\n            for i, long in self.utl3:\n                self.data.write_int(i)\n                self.data.write_long(long)\n\n            self.data.write_int(130100)\n\n        if self.game_version >= 130301:\n            self.data.write_int(len(self.ustid1))\n            for key, (v1, v2) in self.ustid1.items():\n                self.data.write_string(key)\n                self.data.write_int(v1)\n                self.data.write_double(v2)\n\n            self.data.write_int(130301)\n\n        if self.game_version >= 130400:\n            self.data.write_double(self.ud13)\n            self.data.write_double(self.ud14)\n\n            self.data.write_int(130400)\n\n        if self.game_version >= 130500:\n            self.data.write_short(len(self.utl4))\n            for id, ls2 in self.utl4:\n                self.data.write_byte(id)\n                self.data.write_byte(len(ls2))\n                for v1, v2, v3, ls1 in ls2:\n                    self.data.write_byte(v1)\n                    self.data.write_byte(v2)\n                    self.data.write_byte(v3)\n\n                    self.data.write_short(len(ls1))\n\n                    for val in ls1:\n                        self.data.write_short(val)\n\n            self.data.write_int(130500)\n\n        if self.game_version >= 130600:\n            self.data.write_byte(self.uby14)\n\n            if self.not_jp():\n                self.data.write_short(self.ush12)\n\n            self.data.write_int(130600)\n\n        if self.game_version >= 130700:\n            if self.is_jp():\n                self.data.write_short(self.ush12)\n\n            self.data.write_double(self.ud15)\n            self.data.write_byte(self.uby15)\n            self.data.write_byte(self.uby16)\n\n            self.data.write_short(self.ush11)\n            self.data.write_byte(self.uby17)\n            self.data.write_byte(self.uby18)\n            self.data.write_byte(self.uby19)\n\n            self.data.write_double(self.ud16)\n\n            self.data.write_short(len(self.ushd1))\n\n            for key, (value, value_2, data_2) in self.ushd1.items():\n                self.data.write_short(key)\n                self.data.write_short(value)\n                self.data.write_int(value_2)\n\n                self.data.write_short(len(data_2))\n\n                for key2, value3 in data_2.items():\n                    self.data.write_short(key2)\n                    self.data.write_short(value3)\n\n            self.data.write_int(130700)\n        if self.game_version >= 140000:\n            self.data.write_int(self.ui22)\n            self.data.write_double(self.ud17)\n            self.data.write_byte(self.uby20)\n\n            self.data.write_byte(len(self.uild1))\n\n            for key, value in self.uild1.items():\n                self.data.write_int(key)\n                self.data.write_byte(len(value))\n                for val in value:\n                    self.data.write_byte(val)\n\n            self.dojo_chapters.write(self.data)\n\n            self.data.write_short(len(self.uil9))\n            for val in self.uil9:\n                self.data.write_int(val)\n\n            self.data.write_bool(self.ub35)\n            self.data.write_double(self.ud18)\n\n            self.data.write_short(len(self.ushd2))\n\n            for key, value in self.ushd2.items():\n                self.data.write_short(key)\n                self.data.write_byte(value)\n\n            self.data.write_int(140000)\n\n        if self.game_version >= 140100 and self.game_version < 140500:\n            self.data.write_byte(self.uby21)\n            self.data.write_int(140100)\n\n        if self.game_version >= 140200:\n            self.data.write_byte(len(self.uil10))\n\n            for v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13 in self.uil10:\n                self.data.write_int(v1)\n                self.data.write_int(v2)\n                self.data.write_bool(v3)\n                self.data.write_bool(v4)\n                self.data.write_bool(v5)\n                self.data.write_int(v6)\n                self.data.write_int(v7)\n                self.data.write_int(v8)\n                self.data.write_bool(v9)\n                self.data.write_bool(v10)\n                self.data.write_bool(v11)\n                if self.game_version >= 140500:\n                    # game seems to write more than this, may not work with all saves\n                    self.data.write_string(v12 or \"\")\n\n                self.data.write_bool(v13)\n\n            self.data.write_byte(len(self.uid1))\n\n            for key, value in self.uid1.items():\n                self.data.write_int(key)\n                self.data.write_double(value)\n\n            self.data.write_int(self.hundred_million_ticket)\n            self.data.write_int(140200)\n\n        if self.game_version >= 140300:\n            self.data.write_byte(len(self.uil11))\n            for val in self.uil11:\n                self.data.write_byte(val)\n\n            if self.game_version >= 150300:\n                self.data.write_int(self.ui24)\n                self.data.write_bool(self.ub38)\n\n            self.data.write_bool(self.ub36)\n\n            self.data.write_byte(len(self.treasure_chests))\n\n            for val in self.treasure_chests:\n                self.data.write_int(val)\n\n            self.data.write_int(self.ui23)\n            self.data.write_short(len(self.uil13))\n\n            for val in self.uil13:\n                self.data.write_int(val)\n\n            self.data.write_bool(self.ub37)\n            self.data.write_int(140300)\n\n        self.data.write_bytes(self.remaining_data)\n\n        self.data.end_buffer()\n\n    def to_data(self) -> core.Data:\n        dt = core.Data()\n        self.save_wrapper(dt)\n        self.set_hash(add=True)\n        return dt\n\n    def save_wrapper(self, data: core.Data) -> None:\n        try:\n            self.save(data)\n        except Exception as e:\n            raise FailedToSaveError(\n                core.core_data.local_manager.get_key(\"failed_to_save_save\")\n            ) from e\n\n    def to_file_thread(self, path: core.Path):\n        core.Thread(\"to_file\", self.to_file, [path]).start()\n\n    def to_file(self, path: core.Path) -> None:\n        path.parent().generate_dirs()\n        dt = self.to_data()\n        try:\n            dt.to_file(path)\n        except Exception as e:\n            print(e)\n\n    @staticmethod\n    def get_temp_path() -> core.Path:\n        save_temp_path = core.Path.get_data_folder().add(\"save.temp\")\n        save_temp_path.parent().generate_dirs()\n        return save_temp_path\n\n    def to_dict(self) -> dict[str, Any]:\n        data: dict[str, Any] = {\n            \"editor_version\": __version__,\n            \"cc\": self.cc.get_code(),\n            \"dsts\": self.dsts,\n            \"game_version\": self.game_version.game_version,\n            \"ub1\": self.ub1,\n            \"mute_bgm\": self.mute_bgm,\n            \"mute_se\": self.mute_se,\n            \"catfood\": self.catfood,\n            \"current_energy\": self.current_energy,\n            \"year\": self.year,\n            \"month\": self.month,\n            \"day\": self.day,\n            \"timestamp\": self.timestamp,\n            \"date\": self.date.timestamp(),\n            \"ui1\": self.ui1,\n            \"stamp_value_save\": self.stamp_value_save,\n            \"ui2\": self.ui2,\n            \"upgrade_state\": self.upgrade_state,\n            \"xp\": self.xp,\n            \"tutorial_state\": self.tutorial_state,\n            \"ui3\": self.ui3,\n            \"koreaSuperiorTreasureState\": self.koreaSuperiorTreasureState,\n            \"unlock_popups_11\": self.unlock_popups_11,\n            \"ui5\": self.ui5,\n            \"unlock_enemy_guide\": self.unlock_enemy_guide,\n            \"ui6\": self.ui6,\n            \"ub0\": self.ub0,\n            \"ui7\": self.ui7,\n            \"cleared_eoc_1\": self.cleared_eoc_1,\n            \"ui8\": self.ui8,\n            \"unlocked_ending\": self.unlocked_ending,\n            \"lineups\": self.lineups.serialize(),\n            \"stamp_data\": self.stamp_data.serialize(),\n            \"story\": self.story.serialize(),\n            \"enemy_guide\": self.enemy_guide,\n            \"cats\": self.cats.serialize(),\n            \"special_skills\": self.special_skills.serialize(),\n            \"menu_unlocks\": self.menu_unlocks,\n            \"unlock_popups_0\": self.unlock_popups_0,\n            \"battle_items\": self.battle_items.serialize(),\n            \"new_dialogs_2\": self.new_dialogs_2,\n            \"uil1\": self.uil1,\n            \"moneko_bonus\": self.moneko_bonus,\n            \"daily_reward_initialized\": self.daily_reward_initialized,\n            \"date_2\": self.date_2.timestamp(),\n            \"date_3\": self.date_3.timestamp(),\n            \"ui0\": self.ui0,\n            \"stage_unlock_cat_value\": self.stage_unlock_cat_value,\n            \"show_ending_value\": self.show_ending_value,\n            \"chapter_clear_cat_unlock\": self.chapter_clear_cat_unlock,\n            \"ui9\": self.ui9,\n            \"ios_android_month\": self.ios_android_month,\n            \"ui10\": self.ui10,\n            \"save_data_4_hash\": self.save_data_4_hash,\n            \"mysale\": self.mysale.serialize(),\n            \"chara_flags\": self.chara_flags,\n            \"uim1\": self.uim1,\n            \"ubm1\": self.ubm1,\n            \"chara_flags_2\": self.chara_flags_2,\n            \"normal_tickets\": self.normal_tickets,\n            \"rare_tickets\": self.rare_tickets,\n            \"event_stages\": self.event_stages.serialize(),\n            \"itf1_ending\": self.itf1_ending,\n            \"continue_flag\": self.continue_flag,\n            \"unlock_popups_8\": self.unlock_popups_8,\n            \"unit_drops\": self.unit_drops,\n            \"gatya\": self.gatya.serialize(),\n            \"get_event_data\": self.get_event_data,\n            \"achievements\": self.achievements,\n            \"os_value\": self.os_value,\n            \"date_4\": self.date_4.timestamp(),\n            \"player_id\": self.player_id,\n            \"order_ids\": self.order_ids,\n            \"g_timestamp\": self.g_timestamp,\n            \"g_servertimestamp\": self.g_servertimestamp,\n            \"m_gettimesave\": self.m_gettimesave,\n            \"usl1\": self.usl1,\n            \"energy_notification\": self.energy_notification,\n            \"full_gameversion\": self.full_gameversion,\n            \"uil2\": self.uil2,\n            \"uil3\": self.uil3,\n            \"uil4\": self.uil4,\n            \"g_timestamp_2\": self.g_timestamp_2,\n            \"g_servertimestamp_2\": self.g_servertimestamp_2,\n            \"m_gettimesave_2\": self.m_gettimesave_2,\n            \"unknown_timestamp\": self.unknown_timestamp,\n            \"usl2\": self.usl2,\n            \"m_dGetTimeSave2\": self.m_dGetTimeSave2,\n            \"ui11\": self.ui11,\n            \"ubl1\": self.ubl1,\n            \"user_rank_rewards\": self.user_rank_rewards.serialize(),\n            \"transfer_code\": self.transfer_code,\n            \"confirmation_code\": self.confirmation_code,\n            \"transfer_flag\": self.transfer_flag,\n            \"item_reward_stages\": self.item_reward_stages.serialize(),\n            \"timed_score_stages\": self.timed_score_stages.serialize(),\n            \"inquiry_code\": self.inquiry_code,\n            \"officer_pass\": self.officer_pass.serialize(),\n            \"has_account\": self.has_account,\n            \"backup_state\": self.backup_state,\n            \"ub2\": self.ub2,\n            \"itf1_complete\": self.itf1_complete,\n            \"title_chapter_bg\": self.title_chapter_bg,\n            \"combo_unlocks\": self.combo_unlocks,\n            \"combo_unlocked_10k_ur\": self.combo_unlocked_10k_ur,\n            \"event_capsules\": self.event_capsules,\n            \"event_capsules_counter\": self.event_capsules_counter,\n            \"m_dGetTimeSave3\": self.m_dGetTimeSave3,\n            \"gatya_seen_lucky_drops\": self.gatya_seen_lucky_drops,\n            \"banned\": self.show_ban_message,\n            \"catfood_beginner_purchased\": self.catfood_beginner_purchased,\n            \"next_week_timestamp\": self.next_week_timestamp,\n            \"catfood_beginner_expired\": self.catfood_beginner_expired,\n            \"rank_up_sale_value\": self.rank_up_sale_value,\n            \"time_since_time_check_cumulative\": self.time_since_time_check_cumulative,\n            \"server_timestamp\": self.server_timestamp,\n            \"last_checked_energy_recovery_time\": self.last_checked_energy_recovery_time,\n            \"time_since_check\": self.time_since_check,\n            \"last_checked_expedition_time\": self.last_checked_expedition_time,\n            \"catfruit\": self.catfruit,\n            \"catseyes\": self.catseyes,\n            \"catamins\": self.catamins,\n            \"gamatoto\": self.gamatoto.serialize(),\n            \"unlock_popups_6\": self.unlock_popups_6,\n            \"ex_stages\": self.ex_stages.serialize(),\n            \"item_pack\": self.item_pack.serialize(),\n            \"platinum_tickets\": self.platinum_tickets,\n            \"logins\": self.logins.serialize(),\n            \"reset_item_reward_flags\": self.reset_item_reward_flags,\n            \"reward_remaining_time\": self.reward_remaining_time,\n            \"last_checked_reward_time\": self.last_checked_reward_time,\n            \"announcements\": self.announcements,\n            \"backup_counter\": self.backup_counter,\n            \"ui12\": self.ui12,\n            \"ui13\": self.ui13,\n            \"ui14\": self.ui14,\n            \"ub3\": self.ub3,\n            \"backup_frame\": self.backup_frame,\n            \"ub4\": self.ub4,\n            \"dojo\": self.dojo.serialize(),\n            \"last_checked_zombie_time\": self.last_checked_zombie_time,\n            \"outbreaks\": self.outbreaks.serialize(),\n            \"scheme_items\": self.scheme_items.serialize(),\n            \"first_locks\": self.first_locks,\n            \"energy_penalty_timestamp\": self.energy_penalty_timestamp,\n            \"shown_maxcollab_mg\": self.shown_maxcollab_mg,\n            \"unlock_popups\": self.unlock_popups.serialize(),\n            \"ototo\": self.ototo.serialize(),\n            \"last_checked_castle_time\": self.last_checked_castle_time,\n            \"beacon_base\": self.beacon_base.serialize(),\n            \"tower\": self.tower.serialize(),\n            \"missions\": self.missions.serialize(),\n            \"challenge\": self.challenge.serialize(),\n            \"event_update_flags\": self.event_update_flags,\n            \"cotc_1_complete\": self.cotc_1_complete,\n            \"map_resets\": self.map_resets.serialize(),\n            \"uncanny\": self.uncanny.serialize(),\n            \"catamin_stages\": self.catamin_stages.serialize(),\n            \"lucky_tickets\": self.lucky_tickets,\n            \"ub5\": self.ub5,\n            \"np\": self.np,\n            \"ub6\": self.ub6,\n            \"ub7\": self.ub7,\n            \"leadership\": self.leadership,\n            \"filibuster_stage_id\": self.filibuster_stage_id,\n            \"filibuster_stage_enabled\": self.filibuster_stage_enabled,\n            \"stage_ids_10s\": self.stage_ids_10s,\n            \"uil6\": self.uil6,\n            \"legend_quest\": self.legend_quest.serialize(),\n            \"ush1\": self.ush1,\n            \"uby1\": self.uby1,\n            \"uiid1\": self.uiid1,\n            \"uby2\": self.uby2,\n            \"restart_pack\": self.restart_pack,\n            \"medals\": self.medals.serialize(),\n            \"wildcat_slots\": self.wildcat_slots.serialize(),\n            \"ush2\": self.ush2,\n            \"ush3\": self.ush3,\n            \"ui15\": self.ui15,\n            \"ud1\": self.ud1,\n            \"utl1\": self.utl1,\n            \"uidd1\": self.uidd1,\n            \"gauntlets\": self.gauntlets.serialize(),\n            \"enigma_clears\": self.enigma_clears.serialize(),\n            \"enigma\": self.enigma.serialize(),\n            \"cleared_slots\": self.cleared_slots.serialize(),\n            \"collab_gauntlets\": self.collab_gauntlets.serialize(),\n            \"ub8\": self.ub8,\n            \"ud2\": self.ud2,\n            \"ud3\": self.ud3,\n            \"ui16\": self.ui16,\n            \"uby3\": self.uby3,\n            \"ub9\": self.ub9,\n            \"ud4\": self.ud4,\n            \"ud5\": self.ud5,\n            \"uiid3\": self.uiid3,\n            \"uidd2\": self.uidd2,\n            \"uidd3\": self.uidd3,\n            \"talent_orbs\": self.talent_orbs.serialize(),\n            \"uidiid2\": self.uidiid2,\n            \"ub10\": self.ub10,\n            \"uil7\": self.uil7,\n            \"ubl2\": self.ubl2,\n            \"cat_shrine\": self.cat_shrine.serialize(),\n            \"ud6\": self.ud6,\n            \"ud7\": self.ud7,\n            \"legend_tickets\": self.legend_tickets,\n            \"uiil1\": self.uiil1,\n            \"ub11\": self.ub11,\n            \"ub12\": self.ub12,\n            \"password_refresh_token\": self.password_refresh_token,\n            \"ub13\": self.ub13,\n            \"uby4\": self.uby4,\n            \"uby5\": self.uby5,\n            \"ud8\": self.ud8,\n            \"ud9\": self.ud9,\n            \"date_int\": self.date_int,\n            \"event_capsules_2\": self.event_capsules_2,\n            \"two_battle_lines\": self.two_battle_lines,\n            \"ud10\": self.ud10,\n            \"platinum_shards\": self.platinum_shards,\n            \"ub15\": self.ub15,\n            \"cat_scratcher\": self.cat_scratcher.serialize(),\n            \"aku\": self.aku.serialize(),\n            \"ub16\": self.ub16,\n            \"ub17\": self.ub17,\n            \"ushdshd2\": self.ushdshd2,\n            \"ushdd\": self.ushdd,\n            \"ushdd2\": self.ushdd2,\n            \"ub18\": self.ub18,\n            \"uby6\": self.uby6,\n            \"uidtii\": self.uidtii,\n            \"behemoth_culling\": self.behemoth_culling.serialize(),\n            \"ub19\": self.ub19,\n            \"ub20\": self.ub20,\n            \"uidtff\": self.uidtff,\n            \"ub21\": self.ub21,\n            \"dojo_3x_speed\": self.dojo_3x_speed,\n            \"ub22\": self.ub22,\n            \"ub23\": self.ub23,\n            \"ui17\": self.ui17,\n            \"ush4\": self.ush4,\n            \"uby7\": self.uby7,\n            \"uby8\": self.uby8,\n            \"ub24\": self.ub24,\n            \"uby9\": self.uby9,\n            \"ushl1\": self.ushl1,\n            \"ushl2\": self.ushl2,\n            \"ushl3\": self.ushl3,\n            \"ui18\": self.ui18,\n            \"ui19\": self.ui19,\n            \"ui20\": self.ui20,\n            \"ush5\": self.ush5,\n            \"ush6\": self.ush6,\n            \"ush7\": self.ush7,\n            \"ush8\": self.ush8,\n            \"uby10\": self.uby10,\n            \"ub25\": self.ub25,\n            \"ub26\": self.ub26,\n            \"ub27\": self.ub27,\n            \"ub28\": self.ub28,\n            \"ub29\": self.ub29,\n            \"ub30\": self.ub30,\n            \"uby11\": self.uby11,\n            \"ushl4\": self.ushl4,\n            \"ubl3\": self.ubl3,\n            \"labyrinth_medals\": self.labyrinth_medals,\n            \"zero_legends\": self.zero_legends.serialize(),\n            \"uby12\": self.uby12,\n            \"ushl6\": self.ushl6,\n            \"ub31\": self.ub31,\n            \"ush9\": self.ush9,\n            \"ushshd\": self.ushshd,\n            \"ud11\": self.ud11,\n            \"ud12\": self.ud12,\n            \"ub32\": self.ub32,\n            \"ub33\": self.ub33,\n            \"ub34\": self.ub34,\n            \"ui21\": self.ui21,\n            \"golden_cpu_count\": self.golden_cpu_count,\n            \"sound_effects_volume\": self.sound_effects_volume,\n            \"background_music_volume\": self.background_music_volume,\n            \"ustl1\": self.ustl1,\n            \"utl3\": self.utl3,\n            \"ustid1\": self.ustid1,\n            \"ud13\": self.ud13,\n            \"ud14\": self.ud14,\n            \"utl4\": self.utl4,\n            \"uby14\": self.uby14,\n            \"ush12\": self.ush12,\n            \"ud15\": self.ud15,\n            \"uby15\": self.uby15,\n            \"uby16\": self.uby16,\n            \"ush11\": self.ush11,\n            \"uby17\": self.uby17,\n            \"uby18\": self.uby18,\n            \"uby19\": self.uby19,\n            \"ud16\": self.ud16,\n            \"ushd1\": self.ushd1,\n            \"ui22\": self.ui22,\n            \"ud17\": self.ud17,\n            \"uby20\": self.uby20,\n            \"uild1\": self.uild1,\n            \"dojo_chapters\": self.dojo_chapters.serialize(),\n            \"uil9\": self.uil9,\n            \"ub35\": self.ub35,\n            \"ud18\": self.ud18,\n            \"ushd2\": self.ushd2,\n            \"uby21\": self.uby21,\n            \"uil10\": self.uil10,\n            \"uid1\": self.uid1,\n            \"hundred_million_ticket\": self.hundred_million_ticket,\n            \"uil11\": self.uil11,\n            \"ub36\": self.ub36,\n            \"treasure_chests\": self.treasure_chests,\n            \"ui23\": self.ui23,\n            \"uil13\": self.uil13,\n            \"ub37\": self.ub37,\n            \"ub38\": self.ub38,\n            \"ui24\": self.ui24,\n            \"remaining_data\": base64.b64encode(self.remaining_data).decode(\"utf-8\"),\n        }\n        return data\n\n    @staticmethod\n    def from_dict(data: dict[str, Any], warn: bool = True) -> SaveFile:\n        editor_version = data.get(\"editor_version\", \"0.0.0\")\n        if editor_version != __version__ and warn:\n            cli.color.ColoredText.localize(\n                \"editor_version_mismatch\",\n                json_version=editor_version,\n                editor_version=__version__,\n            )\n        cc = data.get(\"cc\")\n        if cc is not None:\n            cc = core.CountryCode(cc)\n        else:\n            cc = None\n        save_file = SaveFile(cc=cc)\n        save_file.dsts = data.get(\"dsts\", [])\n        save_file.game_version = core.GameVersion(data.get(\"game_version\", 0))\n        save_file.ub1 = data.get(\"ub1\", False)\n        save_file.mute_bgm = data.get(\"mute_bgm\", False)\n        save_file.mute_se = data.get(\"mute_se\", False)\n        save_file.catfood = data.get(\"catfood\", 0)\n        save_file.current_energy = data.get(\"current_energy\", 0)\n        save_file.year = data.get(\"year\", 0)\n        save_file.month = data.get(\"month\", 0)\n        save_file.day = data.get(\"day\", 0)\n        save_file.timestamp = data.get(\"timestamp\", 0.0)\n        save_file.date = datetime.datetime.fromtimestamp(data.get(\"date\", 0))\n        save_file.ui1 = data.get(\"ui1\", 0)\n        save_file.stamp_value_save = data.get(\"stamp_value_save\", 0)\n        save_file.ui2 = data.get(\"ui2\", 0)\n        save_file.upgrade_state = data.get(\"upgrade_state\", 0)\n        save_file.xp = data.get(\"xp\", 0)\n        save_file.tutorial_state = data.get(\"tutorial_state\", 0)\n        save_file.ui3 = data.get(\"ui3\", 0)\n        save_file.koreaSuperiorTreasureState = data.get(\"koreaSuperiorTreasureState\", 0)\n        save_file.unlock_popups_11 = data.get(\"unlock_popups_11\", [])\n        save_file.ui5 = data.get(\"ui5\", 0)\n        save_file.unlock_enemy_guide = data.get(\"unlock_enemy_guide\", 0)\n        save_file.ui6 = data.get(\"ui6\", 0)\n        save_file.ub0 = data.get(\"ub0\", False)\n        save_file.ui7 = data.get(\"ui7\", 0)\n        save_file.cleared_eoc_1 = data.get(\"cleared_eoc_1\", 0)\n        save_file.ui8 = data.get(\"ui8\", 0)\n        save_file.unlocked_ending = data.get(\"unlocked_ending\", 0)\n        save_file.lineups = core.LineUps.deserialize(data.get(\"lineups\", {}))\n        save_file.stamp_data = core.StampData.deserialize(data.get(\"stamp_data\", {}))\n        save_file.story = core.StoryChapters.deserialize(data.get(\"story\", []))\n        save_file.enemy_guide = data.get(\"enemy_guide\", [])\n        save_file.cats = core.Cats.deserialize(data.get(\"cats\", {}))\n        save_file.special_skills = core.SpecialSkills.deserialize(\n            data.get(\"special_skills\", [])\n        )\n        save_file.menu_unlocks = data.get(\"menu_unlocks\", [])\n        save_file.unlock_popups_0 = data.get(\"unlock_popups_0\", [])\n        save_file.battle_items = core.BattleItems.deserialize(\n            data.get(\"battle_items\", {})\n        )\n        save_file.new_dialogs_2 = data.get(\"new_dialogs_2\", [])\n        save_file.uil1 = data.get(\"uil1\", [])\n        save_file.moneko_bonus = data.get(\"moneko_bonus\", [])\n        save_file.daily_reward_initialized = data.get(\"daily_reward_initialized\", [])\n        save_file.date_2 = datetime.datetime.fromtimestamp(data.get(\"date_2\", 0))\n        save_file.date_3 = datetime.datetime.fromtimestamp(data.get(\"date_3\", 0))\n        save_file.ui0 = data.get(\"ui0\", 0)\n        save_file.stage_unlock_cat_value = data.get(\"stage_unlock_cat_value\", 0)\n        save_file.show_ending_value = data.get(\"show_ending_value\", 0)\n        save_file.chapter_clear_cat_unlock = data.get(\"chapter_clear_cat_unlock\", 0)\n        save_file.ui9 = data.get(\"ui9\", 0)\n        save_file.ios_android_month = data.get(\"ios_android_month\", 0)\n        save_file.ui10 = data.get(\"ui10\", 0)\n        save_file.save_data_4_hash = data.get(\"save_data_4_hash\", \"\")\n        save_file.mysale = core.MySale.deserialize(data.get(\"mysale\", {}))\n        save_file.chara_flags = data.get(\"chara_flags\", [])\n        save_file.uim1 = data.get(\"uim1\", 0)\n        save_file.ubm1 = data.get(\"ubm1\", False)\n        save_file.chara_flags_2 = data.get(\"chara_flags_2\", [])\n        save_file.normal_tickets = data.get(\"normal_tickets\", 0)\n        save_file.rare_tickets = data.get(\"rare_tickets\", 0)\n        save_file.event_stages = core.EventChapters.deserialize(\n            data.get(\"event_stages\", {})\n        )\n        save_file.itf1_ending = data.get(\"itf1_ending\", 0)\n        save_file.continue_flag = data.get(\"continue_flag\", 0)\n        save_file.unlock_popups_8 = data.get(\"unlock_popups_8\", [])\n        save_file.unit_drops = data.get(\"unit_drops\", [])\n        save_file.gatya = core.Gatya.deserialize(data.get(\"gatya\", {}))\n        save_file.get_event_data = data.get(\"get_event_data\", False)\n        save_file.achievements = data.get(\"achievements\", [])\n        save_file.os_value = data.get(\"os_value\", 0)\n        save_file.date_4 = datetime.datetime.fromtimestamp(data.get(\"date_4\", 0))\n        save_file.player_id = data.get(\"player_id\", \"\")\n        save_file.order_ids = data.get(\"order_ids\", [])\n        save_file.g_timestamp = data.get(\"g_timestamp\", 0.0)\n        save_file.g_servertimestamp = data.get(\"g_servertimestamp\", 0.0)\n        save_file.m_gettimesave = data.get(\"m_gettimesave\", 0.0)\n        save_file.usl1 = data.get(\"usl1\", [])\n        save_file.energy_notification = data.get(\"energy_notification\", False)\n        save_file.full_gameversion = data.get(\"full_gameversion\", 0)\n        save_file.uil2 = data.get(\"uil2\", [])\n        save_file.uil3 = data.get(\"uil3\", [])\n        save_file.uil4 = data.get(\"uil4\", [])\n        save_file.g_timestamp_2 = data.get(\"g_timestamp_2\", 0.0)\n        save_file.g_servertimestamp_2 = data.get(\"g_servertimestamp_2\", 0.0)\n        save_file.m_gettimesave_2 = data.get(\"m_gettimesave_2\", 0.0)\n        save_file.unknown_timestamp = data.get(\"unknown_timestamp\", 0.0)\n        save_file.usl2 = data.get(\"usl2\", [])\n        save_file.m_dGetTimeSave2 = data.get(\"m_dGetTimeSave2\", 0.0)\n        save_file.ui11 = data.get(\"ui11\", 0)\n        save_file.ubl1 = data.get(\"ubl1\", [])\n        save_file.user_rank_rewards = core.UserRankRewards.deserialize(\n            data.get(\"user_rank_rewards\", [])\n        )\n        save_file.transfer_code = data.get(\"transfer_code\", \"\")\n        save_file.confirmation_code = data.get(\"confirmation_code\", \"\")\n        save_file.transfer_flag = data.get(\"transfer_flag\", False)\n        save_file.item_reward_stages = core.ItemRewardChapters.deserialize(\n            data.get(\"item_reward_stages\", {})\n        )\n        save_file.timed_score_stages = core.TimedScoreChapters.deserialize(\n            data.get(\"timed_score_stages\", [])\n        )\n        save_file.inquiry_code = data.get(\"inquiry_code\", \"\")\n        save_file.officer_pass = core.OfficerPass.deserialize(\n            data.get(\"officer_pass\", {})\n        )\n        save_file.has_account = data.get(\"has_account\", False)\n        save_file.backup_state = data.get(\"backup_state\", 0)\n        save_file.ub2 = data.get(\"ub2\", False)\n        save_file.itf1_complete = data.get(\"itf1_complete\", 0)\n        save_file.title_chapter_bg = data.get(\"title_chapter_bg\", 0)\n        save_file.combo_unlocks = data.get(\"combo_unlocks\", [])\n        save_file.combo_unlocked_10k_ur = data.get(\"combo_unlocked_10k_ur\", False)\n        save_file.event_capsules = data.get(\"event_capsules\", [])\n        save_file.event_capsules_counter = data.get(\"event_capsules_counter\", [])\n        save_file.m_dGetTimeSave3 = data.get(\"m_dGetTimeSave3\", 0.0)\n        save_file.gatya_seen_lucky_drops = data.get(\"gatya_seen_lucky_drops\", [])\n        save_file.show_ban_message = data.get(\"banned\", False)\n        save_file.catfood_beginner_purchased = data.get(\n            \"catfood_beginner_purchased\", []\n        )\n        save_file.next_week_timestamp = data.get(\"next_week_timestamp\", 0.0)\n        save_file.catfood_beginner_expired = data.get(\"catfood_beginner_expired\", [])\n        save_file.rank_up_sale_value = data.get(\"rank_up_sale_value\", 0)\n        save_file.time_since_time_check_cumulative = data.get(\n            \"time_since_time_check_cumulative\", 0.0\n        )\n        save_file.server_timestamp = data.get(\"server_timestamp\", 0.0)\n        save_file.last_checked_energy_recovery_time = data.get(\n            \"last_checked_energy_recovery_time\", 0.0\n        )\n        save_file.time_since_check = data.get(\"time_since_check\", 0.0)\n        save_file.last_checked_expedition_time = data.get(\n            \"last_checked_expedition_time\", 0.0\n        )\n        save_file.catfruit = data.get(\"catfruit\", [])\n        save_file.catseyes = data.get(\"catseyes\", [])\n        save_file.catamins = data.get(\"catamins\", [])\n        save_file.gamatoto = core.Gamatoto.deserialize(data.get(\"gamatoto\", {}))\n        save_file.unlock_popups_6 = data.get(\"unlock_popups_6\", [])\n        save_file.ex_stages = core.ExChapters.deserialize(data.get(\"ex_stages\", []))\n        save_file.item_pack = core.ItemPack.deserialize(data.get(\"item_pack\", {}))\n        save_file.platinum_tickets = data.get(\"platinum_tickets\", 0)\n        save_file.logins = core.LoginBonus.deserialize(data.get(\"logins\", {}))\n        save_file.reset_item_reward_flags = data.get(\"reset_item_reward_flags\", [])\n        save_file.reward_remaining_time = data.get(\"reward_remaining_time\", 0.0)\n        save_file.last_checked_reward_time = data.get(\"last_checked_reward_time\", 0.0)\n        save_file.announcements = data.get(\"announcements\", [])\n        save_file.backup_counter = data.get(\"backup_counter\", 0)\n        save_file.ui12 = data.get(\"ui12\", 0)\n        save_file.ui13 = data.get(\"ui13\", 0)\n        save_file.ui14 = data.get(\"ui14\", 0)\n        save_file.ub3 = data.get(\"ub3\", False)\n        save_file.backup_frame = data.get(\"backup_frame\", 0)\n        save_file.ub4 = data.get(\"ub4\", False)\n        save_file.dojo = core.Dojo.deserialize(data.get(\"dojo\", {}))\n        save_file.last_checked_zombie_time = data.get(\"last_checked_zombie_time\", 0.0)\n        save_file.outbreaks = core.Outbreaks.deserialize(data.get(\"outbreaks\", {}))\n        save_file.scheme_items = core.SchemeItems.deserialize(\n            data.get(\"scheme_items\", {})\n        )\n        save_file.first_locks = data.get(\"first_locks\", {})\n        save_file.energy_penalty_timestamp = data.get(\"energy_penalty_timestamp\", 0.0)\n        save_file.shown_maxcollab_mg = data.get(\"shown_maxcollab_mg\", False)\n        save_file.unlock_popups = core.UnlockPopups.deserialize(\n            data.get(\"unlock_popups\", {})\n        )\n        save_file.ototo = core.Ototo.deserialize(data.get(\"ototo\", {}))\n        save_file.last_checked_castle_time = data.get(\"last_checked_castle_time\", 0.0)\n        save_file.beacon_base = core.BeaconEventListScene.deserialize(\n            data.get(\"beacon_base\", {})\n        )\n        save_file.tower = core.TowerChapters.deserialize(data.get(\"tower\", {}))\n        save_file.missions = core.Missions.deserialize(data.get(\"missions\", {}))\n        save_file.challenge = core.ChallengeChapters.deserialize(\n            data.get(\"challenge\", {})\n        )\n        save_file.event_update_flags = data.get(\"event_update_flags\", [])\n        save_file.cotc_1_complete = data.get(\"cotc_1_complete\", False)\n        save_file.map_resets = core.MapResets.deserialize(data.get(\"map_resets\", {}))\n        save_file.uncanny = core.UncannyChapters.deserialize(data.get(\"uncanny\", {}))\n        save_file.catamin_stages = core.UncannyChapters.deserialize(\n            data.get(\"catamin_stages\", {})\n        )\n        save_file.lucky_tickets = data.get(\"lucky_tickets\", [])\n        save_file.ub5 = data.get(\"ub5\", False)\n        save_file.np = data.get(\"np\", 0)\n        save_file.ub6 = data.get(\"ub6\", False)\n        save_file.ub7 = data.get(\"ub7\", False)\n        save_file.leadership = data.get(\"leadership\", 0)\n        save_file.filibuster_stage_id = data.get(\"filibuster_stage_id\", 0)\n        save_file.filibuster_stage_enabled = data.get(\"filibuster_stage_enabled\", False)\n        save_file.stage_ids_10s = data.get(\"stage_ids_10s\", [])\n        save_file.uil6 = data.get(\"uil6\", [])\n        save_file.legend_quest = core.LegendQuestChapters.deserialize(\n            data.get(\"legend_quest\", {})\n        )\n        save_file.ush1 = data.get(\"ush1\", 0)\n        save_file.uby1 = data.get(\"uby1\", 0)\n        save_file.uiid1 = data.get(\"uiid1\", {})\n        save_file.uby2 = data.get(\"uby2\", 0)\n        save_file.restart_pack = data.get(\"restart_pack\", 0)\n        save_file.medals = core.Medals.deserialize(data.get(\"medals\", {}))\n        save_file.wildcat_slots = core.GamblingEvent.deserialize(\n            data.get(\"wildcat_slots\", {})\n        )\n        save_file.ush2 = data.get(\"ush2\", 0)\n        save_file.ush3 = data.get(\"ush3\", 0)\n        save_file.ui15 = data.get(\"ui15\", 0)\n        save_file.ud1 = data.get(\"ud1\", 0.0)\n        save_file.utl1 = data.get(\"utl1\", [])\n        save_file.uidd1 = data.get(\"uidd1\", {})\n        save_file.gauntlets = core.GauntletChapters.deserialize(\n            data.get(\"gauntlets\", {})\n        )\n        save_file.enigma_clears = core.GauntletChapters.deserialize(\n            data.get(\"enigma_clears\", {})\n        )\n        save_file.enigma = core.Enigma.deserialize(data.get(\"enigma\", {}))\n        save_file.cleared_slots = core.ClearedSlots.deserialize(\n            data.get(\"cleared_slots\", {})\n        )\n        save_file.collab_gauntlets = core.GauntletChapters.deserialize(\n            data.get(\"collab_gauntlets\", {})\n        )\n        save_file.ub8 = data.get(\"ub8\", False)\n        save_file.ud2 = data.get(\"ud2\", 0.0)\n        save_file.ud3 = data.get(\"ud3\", 0.0)\n        save_file.ui16 = data.get(\"ui16\", 0)\n        save_file.uby3 = data.get(\"uby3\", 0)\n        save_file.ub9 = data.get(\"ub9\", False)\n        save_file.ud4 = data.get(\"ud4\", 0.0)\n        save_file.ud5 = data.get(\"ud5\", 0.0)\n        save_file.uiid3 = data.get(\"uiid3\", {})\n        save_file.uidd2 = data.get(\"uidd2\", {})\n        save_file.uidd3 = data.get(\"uidd3\", {})\n        save_file.talent_orbs = core.TalentOrbs.deserialize(data.get(\"talent_orbs\", {}))\n        save_file.uidiid2 = data.get(\"uidiid2\", {})\n        save_file.ub10 = data.get(\"ub10\", False)\n        save_file.uil7 = data.get(\"uil7\", [])\n        save_file.ubl2 = data.get(\"ubl2\", [])\n        save_file.cat_shrine = core.CatShrine.deserialize(data.get(\"cat_shrine\", {}))\n        save_file.ud6 = data.get(\"ud6\", 0.0)\n        save_file.ud7 = data.get(\"ud7\", 0)\n        save_file.legend_tickets = data.get(\"legend_tickets\", 0)\n        save_file.uiil1 = data.get(\"uiil1\", [])\n        save_file.ub11 = data.get(\"ub11\", False)\n        save_file.ub12 = data.get(\"ub12\", False)\n        save_file.password_refresh_token = data.get(\"password_refresh_token\", \"\")\n        save_file.ub13 = data.get(\"ub13\", False)\n        save_file.uby4 = data.get(\"uby4\", 0)\n        save_file.uby5 = data.get(\"uby5\", 0)\n        save_file.ud8 = data.get(\"ud8\", 0.0)\n        save_file.ud9 = data.get(\"ud9\", 0.0)\n        save_file.date_int = data.get(\"date_int\", 0)\n        save_file.event_capsules_2 = data.get(\"event_capsules_2\", [])\n        save_file.two_battle_lines = data.get(\"two_battle_lines\", False)\n        save_file.ud10 = data.get(\"ud10\", 0.0)\n        save_file.platinum_shards = data.get(\"platinum_shards\", 0)\n        save_file.ub15 = data.get(\"ub15\", False)\n        save_file.cat_scratcher = core.GamblingEvent.deserialize(\n            data.get(\"cat_scratcher\", {})\n        )\n        save_file.aku = core.AkuChapters.deserialize(data.get(\"aku\", {}))\n        save_file.ub16 = data.get(\"ub16\", False)\n        save_file.ub17 = data.get(\"ub17\", False)\n        save_file.ushdshd2 = data.get(\"ushdshd2\", {})\n        save_file.ushdd = data.get(\"ushdd\", {})\n        save_file.ushdd2 = data.get(\"ushdd2\", {})\n        save_file.ub18 = data.get(\"ub18\", False)\n        save_file.uby6 = data.get(\"uby6\", 0)\n        save_file.uidtii = data.get(\"uidtii\", {})\n        save_file.behemoth_culling = core.GauntletChapters.deserialize(\n            data.get(\"behemoth_culling\", {})\n        )\n        save_file.ub19 = data.get(\"ub19\", False)\n        save_file.ub20 = data.get(\"ub20\", False)\n        save_file.uidtff = data.get(\"uidtff\", {})\n        save_file.ub21 = data.get(\"ub21\", False)\n        save_file.dojo_3x_speed = data.get(\"dojo_3x_speed\", False)\n        save_file.ub22 = data.get(\"ub22\", False)\n        save_file.ub23 = data.get(\"ub23\", False)\n        save_file.ui17 = data.get(\"ui17\", 0)\n        save_file.ush4 = data.get(\"ush4\", 0)\n        save_file.uby7 = data.get(\"uby7\", 0)\n        save_file.uby8 = data.get(\"uby8\", 0)\n        save_file.ub24 = data.get(\"ub24\", False)\n        save_file.uby9 = data.get(\"uby9\", 0)\n        save_file.ushl1 = data.get(\"ushl1\", [])\n        save_file.ushl2 = data.get(\"ushl2\", [])\n        save_file.ushl3 = data.get(\"ushl3\", [])\n        save_file.ui18 = data.get(\"ui18\", 0)\n        save_file.ui19 = data.get(\"ui19\", 0)\n        save_file.ui20 = data.get(\"ui20\", 0)\n        save_file.ush5 = data.get(\"ush5\", 0)\n        save_file.ush6 = data.get(\"ush6\", 0)\n        save_file.ush7 = data.get(\"ush7\", 0)\n        save_file.ush8 = data.get(\"ush8\", 0)\n        save_file.uby10 = data.get(\"uby10\", 0)\n        save_file.ub25 = data.get(\"ub25\", False)\n        save_file.ub26 = data.get(\"ub26\", False)\n        save_file.ub27 = data.get(\"ub27\", False)\n        save_file.ub28 = data.get(\"ub28\", False)\n        save_file.ub29 = data.get(\"ub29\", False)\n        save_file.ub30 = data.get(\"ub30\", False)\n        save_file.uby11 = data.get(\"uby11\", 0)\n        save_file.ushl4 = data.get(\"ushl4\", [])\n        save_file.ubl3 = data.get(\"ubl3\", [])\n        save_file.labyrinth_medals = data.get(\"labyrinth_medals\", [])\n        save_file.zero_legends = core.ZeroLegendsChapters.deserialize(\n            data.get(\"zero_legends\", [])\n        )\n        save_file.uby12 = data.get(\"uby12\", 0)\n        save_file.ushl6 = data.get(\"ushl6\", [])\n        save_file.ub31 = data.get(\"ub31\", False)\n        save_file.ush9 = data.get(\"ush9\", 0)\n        save_file.ushshd = data.get(\"ushshd\", {})\n        save_file.ud11 = data.get(\"ud11\", 0.0)\n        save_file.ud12 = data.get(\"ud12\", 0.0)\n        save_file.ub32 = data.get(\"ub32\", False)\n        save_file.ub33 = data.get(\"ub33\", False)\n        save_file.ub34 = data.get(\"ub34\", False)\n        save_file.ui21 = data.get(\"ui21\", 0)\n        save_file.golden_cpu_count = data.get(\"golden_cpu_count\", 0)\n        save_file.sound_effects_volume = data.get(\"sound_effects_volume\", 0)\n        save_file.background_music_volume = data.get(\"background_music_volume\", 0)\n        save_file.ustl1 = data.get(\"ustl1\", [])\n        save_file.utl3 = data.get(\"utl3\", [])\n        save_file.ustid1 = data.get(\"ustid1\", {})\n        save_file.ud13 = data.get(\"ud13\", 0.0)\n        save_file.ud14 = data.get(\"ud14\", 0.0)\n        save_file.utl4 = data.get(\"utl4\", [])\n        save_file.uby14 = data.get(\"uby14\", 0)\n        save_file.ush12 = data.get(\"ush12\", 0)\n        save_file.ud15 = data.get(\"ud15\", 0.0)\n        save_file.uby15 = data.get(\"uby15\", 0)\n        save_file.uby16 = data.get(\"uby16\", 0)\n        save_file.ush11 = data.get(\"ush11\", 0)\n        save_file.uby17 = data.get(\"uby17\", 0)\n        save_file.uby18 = data.get(\"uby18\", 0)\n        save_file.uby19 = data.get(\"uby19\", 0)\n        save_file.ud16 = data.get(\"ud16\", 0.0)\n        save_file.ushd1 = data.get(\"ushd1\", {})\n        save_file.ui22 = data.get(\"ui22\", 0)\n        save_file.ud17 = data.get(\"ud17\", 0.0)\n        save_file.uby20 = data.get(\"uby20\", 0)\n        save_file.uild1 = data.get(\"uild1\", {})\n        save_file.dojo_chapters = core.ZeroLegendsChapters.deserialize(\n            data.get(\"dojo_chapters\", [])\n        )\n        save_file.uil9 = data.get(\"uil9\", [])\n        save_file.ub35 = data.get(\"ub35\", False)\n        save_file.ud18 = data.get(\"ud18\", 0.0)\n        save_file.ushd2 = data.get(\"ushd2\", {})\n        save_file.uil10 = data.get(\"uil10\", [])\n        save_file.uid1 = data.get(\"uid1\", {})\n        save_file.hundred_million_ticket = data.get(\"hundred_million_ticket\", 0)\n        save_file.uil11 = data.get(\"uil11\", [])\n        save_file.ub36 = data.get(\"ub36\", False)\n        save_file.treasure_chests = data.get(\"treasure_chests\", [])\n        save_file.ui23 = data.get(\"ui23\", 0)\n        save_file.uil13 = data.get(\"uil13\", [])\n        save_file.ub37 = data.get(\"ub37\", False)\n        save_file.ub38 = data.get(\"ub38\", False)\n        save_file.ui24 = data.get(\"ui24\", 0)\n\n        save_file.remaining_data = base64.b64decode(data.get(\"remaining_data\", \"\"))\n\n        return save_file\n\n    def init_save(self, gv: core.GameVersion | None = None):\n        self.dsts = []\n        self.dst_index = 0\n        if gv is None:\n            gv = core.GameVersion(120200)\n        self.set_gv(gv)\n\n        self.ubm1 = False\n        self.ubm = False\n        self.ub0 = False\n        self.ub1 = False\n        self.ub2 = False\n        self.ub3 = False\n        self.ub4 = False\n        self.ub5 = False\n        self.ub6 = False\n        self.ub7 = False\n        self.ub8 = False\n        self.ub9 = False\n        self.ub10 = False\n        self.ub11 = False\n        self.ub12 = False\n        self.ub13 = False\n        self.ub15 = False\n        self.ub16 = False\n        self.ub17 = False\n        self.ub18 = False\n        self.ub19 = False\n        self.ub20 = False\n        self.ub21 = False\n        self.ub22 = False\n        self.ub23 = False\n        self.ub24 = False\n        self.ub25 = False\n        self.ub26 = False\n        self.ub27 = False\n        self.ub28 = False\n        self.ub29 = False\n        self.ub30 = False\n        self.ub31 = False\n        self.ub32 = False\n        self.ub33 = False\n        self.ub34 = False\n        self.ub35 = False\n        self.ub36 = False\n        self.ub37 = False\n        self.ub38 = False\n\n        self.mute_bgm = False\n        self.mute_se = False\n        self.get_event_data = False\n        self.energy_notification = False\n        self.transfer_flag = False\n        self.combo_unlocked_10k_ur = False\n        self.show_ban_message = False\n        self.shown_maxcollab_mg = False\n        self.event_update_flags = False\n        self.filibuster_stage_enabled = False\n        self.dojo_3x_speed = False\n        self.two_battle_lines = False\n\n        self.uim1 = 0\n        self.ui0 = 0\n        self.ui1 = 0\n        self.ui2 = 0\n        self.ui3 = 0\n        self.ui4 = 0\n        self.ui5 = 0\n        self.ui6 = 0\n        self.ui7 = 0\n        self.ui8 = 0\n        self.ui9 = 0\n        self.ui10 = 0\n        self.ui11 = 0\n        self.ui12 = 0\n        self.ui13 = 0\n        self.ui14 = 0\n        self.ui15 = 0\n        self.ui16 = 0\n        self.ui17 = 0\n        self.ui18 = 0\n        self.ui19 = 0\n        self.ui20 = 0\n        self.ui21 = 0\n        self.ui22 = 0\n        self.ui23 = 0\n        self.ui24 = 0\n\n        self.catfood = 0\n        self.current_energy = 0\n        self.year = 0\n        self.month = 0\n        self.day = 0\n        self.stamp_value_save = 0\n        self.upgrade_state = 0\n        self.xp = 0\n        self.tutorial_state = 0\n        self.koreaSuperiorTreasureState = 0\n        self.unlock_enemy_guide = 0\n        self.cleared_eoc_1 = 0\n        self.unlocked_ending = 0\n        self.stage_unlock_cat_value = 0\n        self.show_ending_value = 0\n        self.chapter_clear_cat_unlock = 0\n        self.ios_android_month = 0\n        self.normal_tickets = 0\n        self.rare_tickets = 0\n        self.itf1_ending = 0\n        self.continue_flag = 0\n        self.os_value = 0\n        self.full_gameversion = 0\n        self.backup_state = 0\n        self.itf1_complete = 0\n        self.title_chapter_bg = 0\n        self.rank_up_sale_value = 0\n        self.platinum_tickets = 0\n        self.backup_counter = 0\n        self.backup_frame = 0\n        self.cotc_1_complete = 0\n        self.np = 0\n        self.legend_tickets = 0\n        self.date_int = 0\n        self.platinum_shards = 0\n        self.sound_effects_volume = 0\n        self.background_music_volume = 0\n        self.hundred_million_ticket = 0\n\n        self.ud1 = 0.0\n        self.ud2 = 0.0\n        self.ud3 = 0.0\n        self.ud4 = 0.0\n        self.ud5 = 0.0\n        self.ud6 = 0.0\n        self.ud7 = 0.0\n        self.ud8 = 0.0\n        self.ud9 = 0.0\n        self.ud10 = 0.0\n        self.ud11 = 0.0\n        self.ud12 = 0.0\n        self.ud13 = 0.0\n        self.ud14 = 0.0\n        self.ud15 = 0.0\n        self.ud16 = 0.0\n        self.ud17 = 0.0\n        self.ud18 = 0.0\n\n        self.timestamp = 0.0\n        self.g_timestamp = 0.0\n        self.g_servertimestamp = 0.0\n        self.m_gettimesave = 0.0\n        self.g_timestamp_2 = 0.0\n        self.g_servertimestamp_2 = 0.0\n        self.m_gettimesave_2 = 0.0\n        self.unknown_timestamp = 0.0\n        self.m_dGetTimeSave2 = 0.0\n        self.m_dGetTimeSave3 = 0.0\n        self.next_week_timestamp = 0.0\n        self.time_since_time_check_cumulative = 0.0\n        self.server_timestamp = 0.0\n        self.last_checked_energy_recovery_time = 0.0\n        self.time_since_check = 0.0\n        self.last_checked_expedition_time = 0.0\n        self.reward_remaining_time = 0.0\n        self.last_checked_reward_time = 0.0\n        self.last_checked_zombie_time = 0.0\n        self.energy_penalty_timestamp = 0.0\n        self.last_checked_castle_time = 0.0\n\n        self.date = datetime.datetime.fromtimestamp(0)\n        self.date_2 = datetime.datetime.fromtimestamp(0)\n        self.date_3 = datetime.datetime.fromtimestamp(0)\n        self.date_4 = datetime.datetime.fromtimestamp(0)\n\n        self.uil1 = [0] * 20\n        self.uil2 = [0] * 7\n        self.uil3 = [0] * 7\n        self.uil4 = [0] * 7\n        self.stage_ids_10s = []\n        self.uil6 = []\n        self.uil7 = []\n        self.event_capsules_2 = []\n        self.uil9 = []\n        self.uil10 = []\n        self.uil11 = []\n        self.treasure_chests = []\n        self.uil13 = []\n\n        self.uiil1 = []\n\n        self.usl1 = []\n        self.usl2 = []\n\n        self.ustl1 = []\n\n        if 20 <= gv and gv <= 25:\n            self.ubl1 = [False] * 12\n        else:\n            self.ubl1 = []\n\n        self.ubl2 = [False] * 10\n        self.ubl3 = [False] * 14\n\n        self.ushl1 = []\n        self.ushl2 = []\n        self.ushl3 = []\n        self.ushl4 = []\n        self.ushl6 = []\n\n        self.utl1 = []\n        self.utl3 = []\n        self.utl4 = []\n\n        self.unlock_popups_11 = [0] * 3\n        if 20 <= gv and gv <= 25:\n            self.enemy_guide = [0] * 231\n        else:\n            self.enemy_guide = []\n\n        if gv <= 25:\n            self.menu_unlocks = [0] * 5\n            self.unlock_popups_0 = [0] * 5\n        elif gv <= 26:\n            self.menu_unlocks = [0] * 6\n            self.unlock_popups_0 = [0] * 6\n        else:\n            self.menu_unlocks = []\n            self.unlock_popups_0 = []\n\n        if gv <= 26:\n            self.new_dialogs_2 = [0] * 17\n        else:\n            self.new_dialogs_2 = []\n\n        self.moneko_bonus = [0] * 1\n        self.daily_reward_initialized = [0] * 1\n        self.chara_flags = [0] * 2\n        self.chara_flags_2 = [0] * 2\n\n        self.unlock_popups_8 = [0] * 36\n\n        if 20 <= gv and gv <= 25:\n            self.unit_drops = [0] * 10\n        else:\n            self.unit_drops = []\n\n        self.achievements = [False] * 7\n        self.order_ids = []\n        self.combo_unlocks = []\n        if gv < 34:\n            self.event_capsules = [0] * 100\n            self.event_capsules_counter = [0] * 100\n        else:\n            self.event_capsules = []\n            self.event_capsules_counter = []\n\n        if gv < 26:\n            self.gatya_seen_lucky_drops = [0] * 44\n        else:\n            self.gatya_seen_lucky_drops = []\n        self.catfood_beginner_purchased = [False] * 3\n        self.catfood_beginner_expired = [False] * 3\n        self.catfruit = []\n        self.catseyes = []\n        self.catamins = []\n        self.unlock_popups_6 = []\n        self.reset_item_reward_flags = []\n        self.announcements = [(0, 0)] * 16\n        self.lucky_tickets = []\n        self.labyrinth_medals = []\n\n        self.save_data_4_hash = \"\"\n        self.player_id = \"\"\n        self.transfer_code: str = \"\"\n        self.confirmation_code: str = \"\"\n        self.inquiry_code: str = \"\"\n        self.password_refresh_token: str = \"\"\n\n        self.uby1 = 0\n        self.uby2 = 0\n        self.uby3 = 0\n        self.uby4 = 0\n        self.uby5 = 0\n        self.uby6 = 0\n        self.uby7 = 0\n        self.uby8 = 0\n        self.uby9 = 0\n        self.uby10 = 0\n        self.uby11 = 0\n        self.uby12 = 0\n        self.golden_cpu_count = 0\n        self.uby14 = 0\n        self.uby15 = 0\n        self.uby16 = 0\n        self.uby17 = 0\n        self.uby18 = 0\n        self.uby19 = 0\n        self.uby20 = 0\n        self.uby21 = 0\n\n        self.has_account = 0\n        self.filibuster_stage_id = 0\n        self.restart_pack = 0\n\n        self.ush1 = 0\n        self.ush2 = 0\n        self.ush3 = 0\n        self.ush4 = 0\n        self.ush5 = 0\n        self.ush6 = 0\n        self.ush7 = 0\n        self.ush8 = 0\n        self.ush9 = 0\n        self.ush10 = 0\n        self.ush11 = 0\n        self.ush12 = 0\n\n        self.leadership = 0\n\n        self.lineups = core.LineUps.init(self.game_version)\n        self.stamp_data = core.StampData.init()\n        self.story = core.StoryChapters.init()\n        self.cats = core.Cats.init(self.game_version)\n        self.special_skills = core.SpecialSkills.init()\n        self.battle_items = core.BattleItems.init()\n        self.mysale = core.MySale.init()\n        self.event_stages = core.EventChapters.init(self.game_version)\n        self.gatya = core.Gatya.init()\n        self.user_rank_rewards = core.UserRankRewards.init(self.game_version)\n        self.item_reward_stages = core.ItemRewardChapters.init(self.game_version)\n        self.timed_score_stages = core.TimedScoreChapters.init(self.game_version)\n        self.officer_pass = core.OfficerPass.init()\n        self.gamatoto = core.Gamatoto.init()\n        self.ex_stages = core.ExChapters.init()\n        self.item_pack = core.ItemPack.init()\n        self.logins = core.LoginBonus.init(self.game_version)\n        self.dojo = core.Dojo.init()\n        self.outbreaks = core.Outbreaks.init()\n        self.scheme_items = core.SchemeItems.init()\n        self.unlock_popups = core.UnlockPopups.init()\n        self.ototo = core.Ototo.init(self.game_version)\n        self.beacon_base = core.BeaconEventListScene.init()\n        self.tower = core.TowerChapters.init()\n        self.missions = core.Missions.init()\n        self.challenge = core.ChallengeChapters.init()\n        self.map_resets = core.MapResets.init()\n        self.uncanny = core.UncannyChapters.init()\n        self.catamin_stages = core.UncannyChapters.init()\n        self.legend_quest = core.LegendQuestChapters.init()\n        self.medals = core.Medals.init()\n        self.gauntlets = core.GauntletChapters.init()\n        self.enigma_clears = core.GauntletChapters.init()\n        self.enigma = core.Enigma.init()\n        self.cleared_slots = core.ClearedSlots.init()\n        self.collab_gauntlets = core.GauntletChapters.init()\n        self.talent_orbs = core.TalentOrbs.init()\n        self.cat_shrine = core.CatShrine.init()\n        self.aku = core.AkuChapters.init()\n        self.behemoth_culling = core.GauntletChapters.init()\n        self.zero_legends = core.ZeroLegendsChapters.init()\n        self.dojo_chapters = core.ZeroLegendsChapters.init()\n        self.wildcat_slots = core.GamblingEvent.init()\n        self.cat_scratcher = core.GamblingEvent.init()\n\n        self.uiid1 = {}\n        self.uidd1 = {}\n        self.uidiid2 = {}\n        self.ushdshd2 = {}\n        self.ushdd = {}\n        self.ushdd2 = {}\n        self.uidtii = {}\n        self.uidtff = {}\n        self.ushshd = {}\n        self.ustid1 = {}\n        self.uiid3 = {}\n        self.uidd2 = {}\n        self.uidd3 = {}\n        self.ushd1 = {}\n        self.uild1 = {}\n        self.ushd2 = {}\n        self.uid1 = {}\n\n        self.first_locks = {}\n\n        self.remaining_data = b\"\"\n\n    def is_jp(self) -> bool:\n        return self.cc == core.CountryCodeType.JP\n\n    def not_jp(self) -> bool:\n        return self.cc != core.CountryCodeType.JP\n\n    def is_en(self) -> bool:\n        return self.cc == core.CountryCodeType.EN\n\n    def should_read_dst(self) -> bool:\n        if self.is_jp():\n            return False\n        return self.game_version >= 49\n\n    def read_dst(self):\n        if self.should_read_dst():\n            self.dsts.append(self.data.read_bool())\n\n    def write_dst(self):\n        if self.should_read_dst():\n            try:\n                self.data.write_bool(self.dsts[self.dst_index])\n            except IndexError:\n                self.data.write_bool(False)\n            self.dst_index += 1\n\n    def calculate_user_rank(self):\n        user_rank = 0\n        for cat in self.cats.cats:\n            if not cat.unlocked:\n                continue\n            user_rank += cat.upgrade.base + 1\n            user_rank += cat.upgrade.plus\n\n        for i, skill in enumerate(self.special_skills.skills):\n            if i == 1:\n                continue\n            user_rank += skill.upgrade.base + 1\n            user_rank += skill.upgrade.plus\n\n        return user_rank\n\n    @staticmethod\n    def get_string_identifier(identifier: str) -> str:\n        return f\"_bcsfe:{identifier}\"\n\n    def store_string(self, identifier: str, string: str, overwrite: bool = True):\n        if overwrite:\n            for i, order in enumerate(self.order_ids):\n                if order.startswith(SaveFile.get_string_identifier(identifier)):\n                    self.order_ids[i] = (\n                        f\"{SaveFile.get_string_identifier(identifier)}:{string}\"\n                    )\n                    return\n        self.order_ids.append(f\"{SaveFile.get_string_identifier(identifier)}:{string}\")\n\n    def get_string(self, identifier: str) -> str | None:\n        for order in self.order_ids:\n            if order.startswith(SaveFile.get_string_identifier(identifier)):\n                return order.split(\":\")[2]\n        return None\n\n    def get_strings(self, identifier: str) -> list[str]:\n        strings: list[str] = []\n        for order in self.order_ids:\n            if order.startswith(SaveFile.get_string_identifier(identifier)):\n                strings.append(order.split(\":\")[2])\n\n        return strings\n\n    def remove_string(self, identifier: str):\n        for i, order in enumerate(self.order_ids):\n            if order.startswith(SaveFile.get_string_identifier(identifier)):\n                self.order_ids.pop(i)\n                return\n\n    def remove_strings(self, identifier: str):\n        new_order_ids: list[str] = []\n        for order in self.order_ids:\n            if not order.startswith(SaveFile.get_string_identifier(identifier)):\n                new_order_ids.append(order)\n        self.order_ids = new_order_ids\n\n    def store_dict(\n        self,\n        identifier: str,\n        dictionary: dict[str, str],\n        overwrite: bool = True,\n    ):\n        if overwrite:\n            for i, order in enumerate(self.order_ids):\n                if order.startswith(SaveFile.get_string_identifier(identifier)):\n                    self.order_ids.pop(i)\n\n        for key, value in dictionary.items():\n            self.order_ids.append(\n                f\"{SaveFile.get_string_identifier(identifier)}:{key}:{value}\"\n            )\n\n    def get_dict(self, identifier: str) -> dict[str, str] | None:\n        dictionary: dict[str, str] = {}\n        for order in self.order_ids:\n            if order.startswith(SaveFile.get_string_identifier(identifier)):\n                dictionary[order.split(\":\")[2]] = order.split(\":\")[3]\n\n        return dictionary\n\n    def remove_dict(self, identifier: str):\n        new_order_ids: list[str] = []\n        for order in self.order_ids:\n            if not order.startswith(SaveFile.get_string_identifier(identifier)):\n                new_order_ids.append(order)\n        self.order_ids = new_order_ids\n\n    @staticmethod\n    def get_saves_path() -> core.Path:\n        return core.Path.get_data_folder().add(\"saves\").generate_dirs()\n\n    @staticmethod\n    def get_save_path() -> core.Path:\n        return SaveFile.get_saves_path().add(\"SAVE_DATA\")\n\n    def get_default_path(self) -> core.Path:\n        core.Thread(\"check-backups\", SaveFile.check_backups, []).start()\n        date = datetime.datetime.now().strftime(\"%Y-%m-%d_%H-%M-%S\")\n        local_path = (\n            self.get_saves_path()\n            .add(\"backups\")\n            .add(f\"{self.cc.get_code()}\")\n            .add(self.inquiry_code)\n        )\n        local_path.generate_dirs()\n        local_path = local_path.add(date)\n\n        return local_path\n\n    @staticmethod\n    def check_backups():\n        max_backups = core.core_data.config.get_int(core.ConfigKey.MAX_BACKUPS)\n        if max_backups == -1:\n            return\n        saves_path = SaveFile.get_saves_path().add(\"backups\")\n        saves_path.generate_dirs()\n        all_saves: list[tuple[core.Path, datetime.datetime]] = []\n        for cc in saves_path.get_dirs():\n            for inquiry in cc.get_dirs():\n                for save in inquiry.get_paths_dir():\n                    name = save.basename()\n                    try:\n                        date = datetime.datetime.strptime(name, \"%Y-%m-%d_%H-%M-%S\")\n                    except ValueError:\n                        continue\n                    all_saves.append((save, date))\n\n        all_saves.sort(key=lambda x: x[1], reverse=True)\n        for i, save_info in enumerate(all_saves):\n            if i >= max_backups:\n                save_info[0].remove()\n\n        for cc in saves_path.get_dirs():\n            dirs = cc.get_dirs()\n            if len(dirs) == 0:\n                cc.remove()\n            for inquiry in dirs:\n                saves = inquiry.get_paths_dir()\n                if len(saves) == 0:\n                    inquiry.remove()\n\n    def unlock_equip_menu(self):\n        self.menu_unlocks[2] = max(self.menu_unlocks[2], 1)\n\n    def get_xp(self) -> int:\n        return self.xp\n\n    def set_xp(self, xp: int):\n        self.xp = xp\n\n    def get_catfood(self) -> int:\n        return self.catfood\n\n    def set_catfood(self, catfood: int):\n        self.catfood = catfood\n\n    def get_normal_tickets(self) -> int:\n        return self.normal_tickets\n\n    def set_normal_tickets(self, normal_tickets: int):\n        self.normal_tickets = normal_tickets\n\n    def get_rare_tickets(self) -> int:\n        return self.rare_tickets\n\n    def set_rare_tickets(self, rare_tickets: int):\n        self.rare_tickets = rare_tickets\n\n    def get_platinum_tickets(self) -> int:\n        return self.platinum_tickets\n\n    def set_platinum_tickets(self, platinum_tickets: int):\n        self.platinum_tickets = platinum_tickets\n\n    def get_legend_tickets(self) -> int:\n        return self.legend_tickets\n\n    def set_legend_tickets(self, legend_tickets: int):\n        self.legend_tickets = legend_tickets\n\n    def get_platinum_shards(self) -> int:\n        return self.platinum_shards\n\n    def set_platinum_shards(self, platinum_shards: int):\n        self.platinum_shards = platinum_shards\n\n    def get_np(self) -> int:\n        return self.np\n\n    def set_np(self, np: int):\n        self.np = np\n\n    def get_leadership(self) -> int:\n        return self.leadership\n\n    def set_leadership(self, leadership: int):\n        self.leadership = leadership\n\n    def max_rank_up_sale(self):\n        self.rank_up_sale_value = 0x7FFFFFFF\n"
  },
  {
    "path": "src/bcsfe/core/io/thread_helper.py",
    "content": "from __future__ import annotations\nfrom typing import Callable, Any, Iterable\nimport threading\n\n\nclass Thread:\n    def __init__(\n        self,\n        name: str,\n        target: Callable[..., Any],\n        args: Iterable[Any] | None = None,\n    ):\n        self.name = name\n        self.target = target\n        self.args: Iterable[Any] = args if args is not None else []\n        self._thread: threading.Thread | None = None\n\n    def start(self):\n        self._thread = threading.Thread(\n            target=self.target, args=self.args, name=self.name\n        )\n        self._thread.start()\n\n    def join(self):\n        if self._thread is not None:\n            self._thread.join()\n\n    def is_alive(self) -> bool:\n        if self._thread is not None:\n            return self._thread.is_alive()\n        return False\n\n    @staticmethod\n    def run(name: str, target: Callable[..., None], args: Any):\n        thread = Thread(name, target, args)\n        thread.start()\n        return thread\n\n\ndef thread_run_many_helper(funcs: list[Callable[..., Any]], *args: list[Any]):\n    for i in range(len(funcs)):\n        args_ = args[i]\n        funcs[i](*args_)\n    return\n\n\ndef thread_run_many(\n    funcs: list[Callable[..., Any]], args: Any = None, max_threads: int = 16\n) -> list[Thread]:\n    chunk_size = len(funcs) // max_threads\n    if chunk_size == 0:\n        chunk_size = 1\n    callable_chunks: list[list[Callable[..., Any]]] = []\n    args_chunks: list[list[Any]] = []\n    for i in range(0, len(funcs), chunk_size):\n        callable_chunks.append(funcs[i : i + chunk_size])\n        args_chunks.append(args[i : i + chunk_size])\n\n    threads: list[Thread] = []\n    for i in range(len(callable_chunks)):\n        args_ = args_chunks[i]\n        if args is None:\n            args_ = []\n\n        threads.append(\n            Thread.run(\n                \"run_many_helper\",\n                thread_run_many_helper,\n                (callable_chunks[i], *args_),\n            )\n        )\n\n    for thread in threads:\n        thread.join()\n\n    return threads\n"
  },
  {
    "path": "src/bcsfe/core/io/waydroid.py",
    "content": "from __future__ import annotations\n\nfrom bcsfe import core\nfrom bcsfe.cli import color\nfrom bcsfe.core import io\nfrom bcsfe.core.io.command import CommandResult\n\n\nclass WayDroidNotInstalledError(Exception):\n    def __init__(self, result: CommandResult):\n        self.result = result\n\n\nclass WayDroidHandler(io.root_handler.RootHandler):\n    def __init__(self):\n        self.check_waydroid_installed()\n\n        self.adb_handler = io.adb_handler.AdbHandler(root=False)\n\n        self.package_name = None\n\n    def set_package_name(self, package_name: str):\n        self.package_name = package_name\n        self.adb_handler.set_package_name(self.package_name)\n\n    @staticmethod\n    def display_waydroid_not_installed(e: WayDroidNotInstalledError):\n        color.ColoredText.localize(\"waydroid_not_installed\", error=e)\n        return\n\n    @staticmethod\n    def check_waydroid_installed():\n        result = io.command.Command(\"waydroid -V\").run()\n        if not result.success:\n            raise WayDroidNotInstalledError(result)\n\n    def run_shell_cmd(self, command: str) -> core.CommandResult:\n        cmd = \"waydroid shell\"\n        use_pkexec = core.core_data.config.get_bool(core.ConfigKey.USE_PKEXEC_WAYDROID)\n        if use_pkexec:\n            cmd = \"pkexec \" + cmd\n        return io.command.Command(cmd).run(f\"{command}\")\n\n    def pull_file(\n        self, device_path: core.Path, local_path: core.Path\n    ) -> core.CommandResult:\n        # copy file to sdcard\n\n        result = self.run_shell_cmd(\n            f\"cp {device_path.to_str_forwards()} /sdcard/{device_path.basename()} && chmod o+rw /sdcard/{device_path.basename()}\"\n        )\n\n        if not result.success:\n            return result\n\n        device_path = core.Path(\"/sdcard/\").add(device_path.basename())\n\n        # adb pull\n\n        result = self.adb_handler.adb_pull_file(device_path, local_path)\n        if not result.success:\n            return result\n\n        # delete /sdcard file again\n        #\n        return self.adb_handler.run_shell(f\"rm /sdcard/{device_path.basename()}\")\n\n    def push_file(\n        self, local_path: core.Path, device_path: core.Path\n    ) -> core.CommandResult:\n        original_device_path = device_path.copy_object()\n\n        device_path = core.Path(\"/sdcard/\").add(device_path.basename())\n\n        # push to /sdcard with adb\n\n        import time\n\n        time.sleep(0.25)\n        result = self.adb_handler.adb_push_file(local_path, device_path)\n\n        if not result.success:\n            return result\n\n        result = self.run_shell_cmd(\n            f\"cp '/sdcard/{device_path.basename()}' '{original_device_path.to_str_forwards()}' && chmod o+rw '{original_device_path.to_str_forwards()}'\"\n        )\n\n        if not result.success:\n            return result\n\n        # remove temp file\n        #\n        return self.adb_handler.run_shell(f\"rm '/sdcard/{device_path.basename()}'\")\n\n    def get_battlecats_packages(self) -> list[str]:\n        cmd = \"find /data/data/ -name SAVE_DATA -mindepth 3 -maxdepth 3\"\n        result = self.run_shell_cmd(cmd)\n\n        if not result.success:\n            return []\n\n        packages: list[str] = []\n\n        for package in result.result.split(\"\\n\"):\n            parts = package.split(\"/\")\n            if len(parts) < 4:\n                continue\n\n            packages.append(package.split(\"/\")[3])\n\n        return packages\n\n    def save_battlecats_save(self, local_path: core.Path) -> core.CommandResult:\n        return self.pull_file(self.get_battlecats_save_path(), local_path)\n\n    def load_battlecats_save(self, local_path: core.Path) -> core.CommandResult:\n        return self.push_file(local_path, self.get_battlecats_save_path())\n\n    def run_game(self) -> core.CommandResult:\n        return self.adb_handler.run_game()\n\n    def close_game(self) -> core.CommandResult:\n        return self.adb_handler.close_game()\n"
  },
  {
    "path": "src/bcsfe/core/io/yaml.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe import cli\nimport yaml\n\n\nclass YamlFile:\n    def __init__(self, path: core.Path, print_err: bool = True):\n        self.path = path\n        self.yaml: dict[str, Any] = {}\n        if self.path.exists():\n            self.data = path.read()\n            try:\n                yml = yaml.safe_load(self.data.data)\n                if not isinstance(yml, dict):\n                    self.yaml = {}\n                    self.save(print_err)\n                else:\n                    self.yaml = yml\n            except yaml.YAMLError:\n                self.yaml = {}\n                self.save(print_err)\n        else:\n            self.yaml = {}\n            self.save(print_err)\n\n    def save(self, print_err: bool = True) -> None:\n        self.path.parent().generate_dirs()\n\n        try:\n            with open(self.path.path, \"w\", encoding=\"utf-8\") as f:\n                yaml.dump(self.yaml, f)\n        except FileNotFoundError:\n            if print_err:\n                cli.color.ColoredText.localize(\"yaml_create_error\", path=self.path.path)\n\n    def __getitem__(self, key: str) -> Any:\n        return self.yaml[key]\n\n    def __setitem__(self, key: str, value: Any) -> None:\n        self.yaml[key] = value\n\n    def __delitem__(self, key: str) -> None:\n        del self.yaml[key]\n\n    def __contains__(self, key: str) -> bool:\n        return key in self.yaml\n\n    def __iter__(self):\n        return iter(self.yaml)\n\n    def __len__(self) -> int:\n        return len(self.yaml)\n\n    def __repr__(self) -> str:\n        return self.yaml.__repr__()\n\n    def __str__(self) -> str:\n        return self.yaml.__str__()\n\n    def get(self, key: str) -> Any:\n        return self.yaml.get(key)\n\n    def remove(self) -> None:\n        self.path.remove()\n        self.yaml = {}\n"
  },
  {
    "path": "src/bcsfe/core/locale_handler.py",
    "content": "from __future__ import annotations\nimport dataclasses\nimport tempfile\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\nclass PropertySet:\n    \"\"\"Represents a set of properties in a property file.\"\"\"\n\n    def __init__(self, locale: str, property: str):\n        \"\"\"Initializes a new instance of the PropertySet class.\n\n        Args:\n            locale (str): Language code of the locale.\n            property (str): Name of the property file.\n        \"\"\"\n        self.locale = locale\n        self.property = property\n        self.path = LocalManager.get_locale_folder(locale).add(property + \".properties\")\n        self.properties: dict[str, tuple[str, str]] = {}\n        self.parse()\n\n    def parse(self):\n        \"\"\"Parses the property file.\n\n        Raises:\n            KeyError: If a key is already defined in the property file.\n        \"\"\"\n        lines = self.path.read().to_str().splitlines()\n        i = 0\n        in_multi_line = False\n        multi_line_text = \"\"\n        multi_line_key = \"\"\n\n        while i < len(lines):\n            line = lines[i]\n            finish_multiline = False\n            if (in_multi_line and not line.startswith(\">\")) or (\n                in_multi_line and i == len(lines) - 1\n            ):\n                in_multi_line = False\n                finish_multiline = True\n                if multi_line_key in self.properties:\n                    raise KeyError(\n                        f\"Key {multi_line_key} already exists in property file\"\n                    )\n                if line.startswith(\">\"):\n                    multi_line_text += line[1:]\n                else:\n                    multi_line_text = multi_line_text[:-1]  # remove extra newline\n                self.properties[multi_line_key] = (multi_line_text, self.property)\n                multi_line_text = \"\"\n                multi_line_key = \"\"\n            if line.startswith(\"#\") or not line:\n                i += 1\n                continue\n            if line.startswith(\">\") and in_multi_line:\n                multi_line_text += line[1:] + \"\\n\"\n\n            parts = line.split(\"=\")\n            if line.strip().endswith(\"=\"):\n                in_multi_line = True\n                multi_line_key = parts[0]\n\n            if not in_multi_line and not finish_multiline:\n                key = parts[0]\n                value = \"=\".join(parts[1:])\n                if key in self.properties:\n                    raise KeyError(f\"Key {key} already exists in property file\")\n                self.properties[key] = (value, self.property)\n\n            i += 1\n\n    def get_key(self, key: str) -> str:\n        \"\"\"Gets a key from the property file.\n\n        Args:\n            key (str): Key to get.\n\n        Returns:\n            str: Value of the key.\n        \"\"\"\n        return (\n            self.properties.get(key, key)[0].replace(\"\\\\n\", \"\\n\").replace(\"\\\\t\", \"\\t\")\n        )\n\n    @staticmethod\n    def from_config(property: str) -> PropertySet:\n        \"\"\"Gets a PropertySet from the language code in the config.\n\n        Args:\n            property (str): Name of the property file.\n\n        Returns:\n            PropertySet: PropertySet for the property file.\n        \"\"\"\n        return PropertySet(\n            core.core_data.config.get_str(core.ConfigKey.LOCALE), property\n        )\n\n\nclass LocalManager:\n    \"\"\"Manages properties for a locale\"\"\"\n\n    def __init__(self, locale: str | None = None):\n        \"\"\"Initializes a new instance of the LocalManager class.\n\n        Args:\n            locale (str): Language code of the locale.\n        \"\"\"\n        if locale is None:\n            lc = core.core_data.config.get_str(core.ConfigKey.LOCALE)\n        else:\n            lc = locale\n\n        self.locale = lc\n        self.path = LocalManager.get_locale_folder(lc)\n        self.properties: dict[str, PropertySet] = {}\n        self.all_properties: dict[str, tuple[str, str]] = {}\n        self.en_properties: dict[str, tuple[str, str]] = {}\n        self.en_properties_path = LocalManager.get_locale_folder(\"en\")\n        self.authors: list[str] = [\"fieryhenry\"]\n        self.name: str = \"English\"\n        self.parse()\n        if self.locale == \"en\":\n            self.en_properties = self.all_properties\n\n        if core.core_data.config.get_bool(core.ConfigKey.SHOW_MISSING_LOCALE_KEYS):\n            key = self.get_key(\"missing_locale_keys\")\n            print(key)\n            print()\n            missing = self.get_missing_keys()\n            for key in missing:\n                print(f\"{key[2]}\\n{key[0]}={key[1]}\\n\")\n            if not missing:\n                print(self.get_key(\"none\"))\n\n            print()\n\n            key = self.get_key(\"extra_locale_keys\")\n            print(key)\n            print()\n            extra = self.get_extra_keys()\n            for key in extra:\n                print(f\"{key[2]}\\n{key[0]}={key[1]}\\n\")\n            if not extra:\n                print(self.get_key(\"none\"))\n\n            print()\n\n    def get_missing_keys(self) -> list[tuple[str, str, str]]:\n        missing = set(self.en_properties.keys()) - set(self.all_properties.keys())\n\n        return [\n            (\n                key,\n                self.en_properties[key][0],\n                self.en_properties[key][1] + \".properties\",\n            )\n            for key in missing\n        ]\n\n    def get_extra_keys(self) -> list[tuple[str, str, str]]:\n        extra = set(self.all_properties.keys()) - set(self.en_properties.keys())\n\n        return [\n            (\n                key,\n                self.all_properties[key][0],\n                self.all_properties[key][1] + \".properties\",\n            )\n            for key in extra\n        ]\n\n    def parse(self):\n        \"\"\"Parses all property files in the locale folder recursively.\"\"\"\n        for file in self.path.glob(\"**/*.properties\", recursive=True):\n            file_name = file.strip_path_from(self.path).path\n            property_set = PropertySet(self.locale, file_name[:-11])\n            self.all_properties.update(property_set.properties)\n            self.properties[file_name[:-11]] = property_set\n\n        metadata_path = self.path.add(\"metadata.json\")\n\n        if metadata_path.exists():\n            data = core.JsonFile.from_path(metadata_path)\n            self.authors = data.get(\"authors\") or [\"fieryhenry\"]\n            self.name = data.get(\"name\") or \"English\"\n\n        if self.locale != \"en\":\n            for file in self.en_properties_path.glob(\"**/*.properties\", recursive=True):\n                file_name = file.strip_path_from(self.en_properties_path).path\n                property_set = PropertySet(\"en\", file_name[:-11])\n                self.en_properties.update(property_set.properties)\n\n    def get_key(self, key: str, escape: bool = True, **kwargs: Any) -> str:\n        \"\"\"Gets a key from the property file.\n\n        Args:\n            key (str): Key to get.\n\n        Returns:\n            str: Value of the key.\n        \"\"\"\n        try:\n            text = self.get_key_recursive(key, kwargs, escape)\n        except RecursionError:\n            text = key\n\n        for kwarg_key, kwarg_value in kwargs.items():\n            value = str(kwarg_value)\n            if escape:\n                value = LocalManager.escape_string(value)\n            text = text.replace(\"{\" + kwarg_key + \"}\", value)\n\n        if \"$(\" in text:\n            text = self.parse_condition(text, kwargs)\n\n        return text\n\n    def parse_condition(self, text: str, kwargs: dict[str, Any]) -> str:\n        counter = 0\n        final_text = \"\"\n        in_expression = False\n        expression_text = \"\"\n        count_down = 0\n        while counter < len(text):\n            char = text[counter]\n            if counter == len(text) - 1:\n                final_text += char\n                break\n            next_char = text[counter + 1]\n            if char == \"\\\\\":\n                final_text += next_char\n                counter += 2\n                continue\n            if char == \"$\" and next_char == \"(\":\n                count_down = 0\n                in_expression = True\n            elif char == \"/\" and next_char == \"$\":\n                count_down = 2\n                in_expression = False\n                if len(expression_text) < 3:\n                    counter += 1\n                    continue\n                new_expression_text = expression_text[2:-1]\n                expression_text = \"\"\n                parts = new_expression_text.split(\":\")\n                if len(parts) < 2:\n                    counter += 1\n                    continue\n                keyword = parts[0].strip()\n                expression = parts[1].strip()\n                conditions = expression.split(\"$,\")\n                string = \"\"\n                for i, condition in enumerate(conditions):\n                    condition = condition.strip()\n                    if not condition:\n                        continue\n                    if i == len(conditions) - 1:\n                        string = condition\n                        break\n                    condition_parts = condition.split(\"($\")\n                    if len(condition_parts) < 2:\n                        continue\n                    logic = condition_parts[0].strip()\n                    word = condition_parts[1].strip()\n                    if not word:\n                        continue\n                    word = word[:-1]\n                    value = kwargs.get(keyword)\n                    if value is None:\n                        continue\n                    equality = None\n                    if logic.startswith(\"==\"):\n                        equality = \"==\"\n                    elif logic.startswith(\"!=\"):\n                        equality = \"!=\"\n                    elif logic.startswith(\">=\"):\n                        equality = \">=\"\n                    elif logic.startswith(\"<=\"):\n                        equality = \"<=\"\n                    elif logic.startswith(\">\"):\n                        equality = \">\"\n                    elif logic.startswith(\"<\"):\n                        equality = \"<\"\n                    if equality is None:\n                        continue\n                    logic_parts = logic.split(equality)\n                    if len(logic_parts) < 2:\n                        continue\n                    logic_value = logic_parts[1].strip()\n\n                    if isinstance(value, int):\n                        if not logic_value.isdigit():\n                            continue\n                        logic_value = int(logic_value)\n\n                    if equality == \"==\":\n                        if logic_value == value:\n                            string = word\n                            break\n                    elif equality == \"!=\":\n                        if logic_value != value:\n                            string = word\n                            break\n\n                    if isinstance(logic_value, int) and not string:\n                        if equality == \">\":\n                            if logic_value > value:\n                                string = word\n                                break\n                        elif equality == \">=\":\n                            if logic_value >= value:\n                                string = word\n                                break\n                        elif equality == \"<\":\n                            if logic_value < value:\n                                string = word\n                                break\n                        elif equality == \"<=\":\n                            if logic_value <= value:\n                                string = word\n                                break\n\n                final_text += string\n\n            if in_expression:\n                expression_text += char\n            else:\n                if count_down <= 0:\n                    final_text += char\n                else:\n                    count_down -= 1\n\n            counter += 1\n\n        return final_text\n\n    @staticmethod\n    def get_special_chars() -> list[str]:\n        return [\"<\", \">\", \"/\"]\n\n    @staticmethod\n    def escape_string(string: str) -> str:\n        for char in LocalManager.get_special_chars():\n            string = string.replace(char, \"\\\\\" + char)\n        return string\n\n    def get_key_recursive(\n        self,\n        key: str,\n        kwargs: dict[str, Any],\n        escape: bool = True,\n    ) -> str:\n        value = self.all_properties.get(key)\n        if value is None:\n            value = self.en_properties.get(key, (key, key))\n        value = value[0].replace(\"\\\\n\", \"\\n\").replace(\"\\\\t\", \"\\t\")\n        # replace {{key}} with the value of the key\n        if \"{{\" not in value:\n            return value\n        char_index = 0\n        while char_index < len(value):\n            if value[char_index] == \"{\" and value[char_index + 1] == \"{\":\n                key_name = \"\"\n                char_index += 2\n                while value[char_index] != \"}\":\n                    key_name += value[char_index]\n                    char_index += 1\n\n                if key_name != key:\n                    value = value.replace(\n                        \"{{\" + key_name + \"}}\",\n                        self.get_key(key_name, escape, **kwargs),\n                    )\n            char_index += 1\n\n        return value\n\n    @staticmethod\n    def get_all_aliases(value: str) -> list[str]:\n        \"\"\"Gets all aliases from a string. Aliases are separated by |.\n\n        Args:\n            value (str): String to get aliases from.\n\n        Returns:\n            list[str]: List of aliases.\n        \"\"\"\n        if \"|\" not in value:\n            return [value]\n        i = 0\n        aliases: list[str] = []\n        while i < len(value):\n            char = value[i]\n            prev_char = value[i - 1] if i > 0 else \"\"\n            if char == \"|\" and prev_char != \"\\\\\":\n                aliases.append(value[:i])\n                value = value[i + 1 :]\n                i = 0\n            i += 1\n\n        aliases.append(value)\n        return aliases\n\n    @staticmethod\n    def from_config() -> LocalManager:\n        \"\"\"Gets a LocalManager from the language code in the config.\n\n        Returns:\n            LocalManager: LocalManager for the locale.\n        \"\"\"\n        return LocalManager(core.core_data.config.get_str(core.ConfigKey.LOCALE))\n\n    def check_duplicates(self):\n        \"\"\"Checks for duplicate keys in all property files.\n\n        Raises:\n            KeyError: If a key is already defined in the property file.\n        \"\"\"\n        keys: set[str] = set()\n        for property in self.properties.values():\n            for key in property.properties.keys():\n                if key in keys:\n                    raise KeyError(f\"Duplicate key {key}\")\n                keys.add(key)\n\n    @staticmethod\n    def get_all_locales() -> list[str]:\n        \"\"\"Gets all locales in the locales folder.\n\n        Returns:\n            list[str]: List of locales.\n        \"\"\"\n        locales: list[str] = []\n        for folder in LocalManager.get_locales_folder().get_dirs():\n            locales.append(folder.basename())\n        for folder in LocalManager.get_external_locales_folder().get_dirs():\n            locales.append(folder.basename())\n        return locales\n\n    @staticmethod\n    def get_locales_folder() -> core.Path:\n        \"\"\"Gets the locales folder.\n\n        Returns:\n            core.Path: Path to the locales folder.\n        \"\"\"\n        return core.Path(\"locales\", True)\n\n    @staticmethod\n    def get_external_locales_folder() -> core.Path:\n        \"\"\"Gets the external locales folder.\n\n        Returns:\n            core.Path: Path to the external locales folder.\n        \"\"\"\n        return core.Path.get_data_folder().add(\"external_locales\")\n\n    @staticmethod\n    def get_locale_folder(locale: str) -> core.Path:\n        \"\"\"Gets the folder for a locale.\n\n        Args:\n            locale (str): Language code of the locale.\n\n        Returns:\n            core.Path: Path to the locale folder.\n        \"\"\"\n        if locale.startswith(\"ext-\"):\n            return LocalManager.get_external_locales_folder().add(locale)\n        return LocalManager.get_locales_folder().add(locale)\n\n    @staticmethod\n    def remove_locale(locale: str):\n        \"\"\"Removes a locale.\n\n        Args:\n            locale (str): Language code of the locale.\n        \"\"\"\n        if locale not in LocalManager.get_all_locales():\n            return\n        if locale.startswith(\"ext-\"):\n            extern = ExternalLocaleManager.get_external_locale(locale)\n            if extern is not None:\n                ExternalLocaleManager.delete_locale(extern)\n            LocalManager.get_external_locales_folder().add(locale).remove()\n        else:\n            LocalManager.get_locales_folder().add(locale).remove()\n\n        if core.core_data.config.get_str(core.ConfigKey.LOCALE) == locale:\n            core.core_data.config.set(core.ConfigKey.LOCALE, \"en\")\n\n\n@dataclasses.dataclass\nclass ExternalLocale:\n    short_name: str\n    name: str\n    description: str\n    author: str\n    version: str\n    git_repo: str | None = None\n\n    def to_json(self) -> dict[str, Any]:\n        return dataclasses.asdict(self)\n\n    @staticmethod\n    def from_json(json_data: dict[str, Any]) -> ExternalLocale | None:\n        short_name = json_data.get(\"short_name\")\n        name = json_data.get(\"name\")\n        description = json_data.get(\"description\")\n        author = json_data.get(\"author\")\n        version = json_data.get(\"version\")\n        git_repo = json_data.get(\"git_repo\")\n        if (\n            short_name is None\n            or name is None\n            or description is None\n            or author is None\n            or version is None\n        ):\n            return None\n        return ExternalLocale(\n            short_name,\n            name,\n            description,\n            author,\n            version,\n            git_repo,\n        )\n\n    @staticmethod\n    def from_git_repo(git_repo: str) -> ExternalLocale | None:\n        repo = core.GitHandler().get_repo(git_repo)\n        if repo is None:\n            return None\n        locale_json = repo.get_file(core.Path(\"locale.json\"))\n        if locale_json is None:\n            return None\n        json_data = core.JsonFile.from_data(locale_json).to_object()\n        json_data[\"git_repo\"] = git_repo\n        return ExternalLocale.from_json(json_data)\n\n    def get_new_version(self) -> bool:\n        if self.git_repo is None:\n            return False\n        repo = core.GitHandler().get_repo(self.git_repo)\n        if repo is None:\n            return False\n        with tempfile.TemporaryDirectory() as tmp:\n            temp_dir = core.Path(tmp)\n            success = repo.clone_to_temp(temp_dir)\n            if not success:\n                return False\n            external_locale = ExternalLocaleManager.parse_external_locale(temp_dir)\n            if external_locale is None:\n                return False\n            version = external_locale.version\n\n            if version == self.version:\n                return False\n\n            self.name = external_locale.name\n            self.short_name = external_locale.short_name\n            self.description = external_locale.description\n            self.author = external_locale.author\n            self.version = version\n\n        success = repo.pull()\n        if not success:\n            return False\n        self.save()\n        return True\n\n    def save(self):\n        ExternalLocaleManager.save_locale(self)\n\n    def get_full_name(self) -> str:\n        return f\"ext-{self.author}-{self.short_name}\"\n\n\nclass ExternalLocaleManager:\n    @staticmethod\n    def delete_locale(external_locale: ExternalLocale):\n        if external_locale.git_repo is None:\n            return\n        folder = core.GitHandler.get_repo_folder().add(\n            external_locale.git_repo.split(\"/\")[-1]\n        )\n        folder.remove()\n\n    @staticmethod\n    def save_locale(\n        external_locale: ExternalLocale,\n    ):\n        \"\"\"Saves an external locale.\n\n        Args:\n            external_locale (ExternalLocale): External locale to save.\n        \"\"\"\n        if external_locale.git_repo is None:\n            return\n        folder = LocalManager.get_external_locales_folder().add(\n            external_locale.get_full_name()\n        )\n        folder.generate_dirs()\n\n        repo = core.GitHandler().get_repo(external_locale.git_repo)\n        if repo is None:\n            return\n        files_dir = repo.get_folder(core.Path(\"files\"))\n        if files_dir is None:\n            return\n\n        files_dir.copy_tree(folder)\n\n        json_data = external_locale.to_json()\n        folder.add(\"locale.json\").write(core.JsonFile.from_object(json_data).to_data())\n\n    @staticmethod\n    def parse_external_locale(path: core.Path) -> ExternalLocale | None:\n        \"\"\"Parses an external locale.\n\n        Args:\n            path (core.Path): Path to the external locale.\n\n        Returns:\n            ExternalLocale: External locale.\n        \"\"\"\n        if not path.exists():\n            return None\n        json_data = core.JsonFile.from_data(path.add(\"locale.json\").read()).to_object()\n        return ExternalLocale.from_json(json_data)\n\n    @staticmethod\n    def update_external_locale(external_locale: ExternalLocale):\n        \"\"\"Updates an external locale.\n\n        Args:\n            external_locale (ExternalLocale): External locale to update.\n        \"\"\"\n        if external_locale.git_repo is None:\n            return\n        color.ColoredText.localize(\n            \"checking_for_locale_updates\",\n            locale_name=external_locale.name,\n        )\n        updated = external_locale.get_new_version()\n        if updated:\n            color.ColoredText.localize(\n                \"external_locale_updated\",\n                locale_name=external_locale.name,\n                version=external_locale.version,\n            )\n        else:\n            color.ColoredText.localize(\n                \"external_locale_no_update\",\n                locale_name=external_locale.name,\n                version=external_locale.version,\n            )\n        print()\n\n    @staticmethod\n    def update_all_external_locales(_: Any = None):\n        \"\"\"Updates all external locales.\"\"\"\n        dirs = LocalManager.get_external_locales_folder().get_dirs()\n        if not dirs:\n            color.ColoredText.localize(\n                \"no_external_locales\",\n            )\n            return\n        if not core.GitHandler.is_git_installed():\n            color.ColoredText.localize(\n                \"git_not_installed\",\n            )\n            return\n        for folder in dirs:\n            locale = ExternalLocaleManager.parse_external_locale(folder)\n            if locale is None:\n                continue\n            ExternalLocaleManager.update_external_locale(locale)\n\n    @staticmethod\n    def get_external_locale_config() -> ExternalLocale | None:\n        \"\"\"Gets the external locale from the config.\n\n        Returns:\n            ExternalLocale: External locale.\n        \"\"\"\n\n        locale = core.core_data.config.get_str(core.ConfigKey.LOCALE)\n        if not locale.startswith(\"ext-\"):\n            return None\n        return ExternalLocaleManager.parse_external_locale(\n            LocalManager.get_locale_folder(locale)\n        )\n\n    @staticmethod\n    def get_external_locale(locale: str) -> ExternalLocale | None:\n        \"\"\"Gets the external locale from the code.\n\n        Returns:\n            ExternalLocale: External locale.\n        \"\"\"\n\n        if not locale.startswith(\"ext-\"):\n            return None\n        return ExternalLocaleManager.parse_external_locale(\n            LocalManager.get_locale_folder(locale)\n        )\n"
  },
  {
    "path": "src/bcsfe/core/log.py",
    "content": "from __future__ import annotations\n\n\"\"\"Module for handling logging\"\"\"\nimport traceback\nfrom bcsfe import core\nimport time\n\n\nclass Logger:\n    def __init__(self, path: core.Path | None):\n        \"\"\"\n        Initializes a Logger object\n        \"\"\"\n        if path is None:\n            path = Logger.get_log_path()\n        self.log_file = path\n        try:\n            self.log_data = self.log_file.read(True).split(b\"\\n\")\n        except Exception as _:\n            self.log_data = None\n\n    @staticmethod\n    def get_log_path() -> core.Path:\n        return core.Path.get_state_folder().add(\"bcsfe.log\")\n\n    def is_log_enabled(self) -> bool:\n        return self.log_data is not None\n\n    def get_time(self) -> str:\n        \"\"\"\n        Returns the current time in the format: \"HH:MM:SS\"\n\n        Returns:\n            str: The current time\n        \"\"\"\n        return time.strftime(\"%d/%m/%Y %H:%M:%S\", time.localtime())\n\n    def log_debug(self, message: str):\n        \"\"\"\n        Logs a debug message\n\n        Args:\n            message (str): The message to log\n        \"\"\"\n        if self.log_data is None:\n            return\n        self.log_data.append(core.Data(f\"[DEBUG]::{self.get_time()} - {message}\"))\n        self.write()\n\n    def log_info(self, message: str):\n        \"\"\"\n        Logs an info message\n\n        Args:\n            message (str): The message to log\n        \"\"\"\n        if self.log_data is None:\n            return\n        self.log_data.append(core.Data(f\"[INFO]::{self.get_time()} - {message}\"))\n        self.write()\n\n    def log_warning(self, message: str):\n        \"\"\"\n        Logs a warning message\n\n        Args:\n            message (str): The message to log\n        \"\"\"\n        if self.log_data is None:\n            return\n        self.log_data.append(core.Data(f\"[WARNING]::{self.get_time()} - {message}\"))\n        self.write()\n\n    def log_error(self, message: str):\n        \"\"\"\n        Logs an error message\n\n        Args:\n            message (str): The message to log\n        \"\"\"\n        if self.log_data is None:\n            return\n        self.log_data.append(core.Data(f\"[ERROR]::{self.get_time()} - {message}\"))\n        self.write()\n\n    def log_exception(self, exception: Exception, extra_msg: str = \"\"):\n        tb = traceback.format_exc()\n        if tb == \"NoneType: None\\n\":\n            try:\n                raise exception\n            except Exception:\n                tb = traceback.format_exc()\n\n        self.log_error(\n            f\"{extra_msg}: {exception.__class__.__name__}: {exception}\\n{tb}\"\n        )\n\n    def write(self):\n        \"\"\"\n        Writes the log data to the log file\n        \"\"\"\n        if self.log_data is None:\n            return\n        self.log_file.write(core.Data.from_many(self.log_data, core.Data(\"\\n\")).strip())\n\n    def log_no_file_found(self, file_name: str):\n        \"\"\"\n        Logs that a file was not found\n\n        Args:\n            fileName (str): The name of the file\n        \"\"\"\n        self.log_warning(f\"Could not find {file_name}\")\n\n    @staticmethod\n    def get_traceback() -> str:\n        \"\"\"\n        Gets the traceback of the last exception\n\n        Returns:\n            str: The traceback\n        \"\"\"\n        tb = traceback.format_exc()\n        if tb == \"NoneType: None\\n\":\n            return \"\"\n        return tb\n"
  },
  {
    "path": "src/bcsfe/core/max_value_helper.py",
    "content": "from __future__ import annotations\nimport enum\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass MaxValueType(enum.Enum):\n    CATFOOD = \"catfood\"\n    XP = \"xp\"\n    NORMAL_TICKETS = \"normal_tickets\"\n    HUNDRED_MILLION_TICKETS = \"100_million_tickets\"\n    RARE_TICKETS = \"rare_tickets\"\n    PLATINUM_TICKETS = \"platinum_tickets\"\n    LEGEND_TICKETS = \"legend_tickets\"\n    NP = \"np\"\n    LEADERSHIP = \"leadership\"\n    BATTLE_ITEMS = \"battle_items\"\n    CATAMINS = \"catamins\"\n    CATSEYES = \"catseyes\"\n    CATFRUIT = \"catfruit\"\n    BASE_MATERIALS = \"base_materials\"\n    LABYRINTH_MEDALS = \"labyrinth_medals\"\n    TALENT_ORBS = \"talent_orbs\"\n    TREASURE_LEVEL = \"treasure_level\"\n    STAGE_CLEAR_COUNT = \"stage_clear_count\"\n    ITF_TIMED_SCORE = \"itf_timed_score\"\n    EVENT_TICKETS = \"event_tickets\"\n    TREASURE_CHESTS = \"treasure_chests\"\n\n\nclass MaxValueHelper:\n    def __init__(self):\n        self.max_value_data = self.get_max_value_data()\n\n    @staticmethod\n    def convert_val_code(value_code: MaxValueType | str) -> str:\n        if isinstance(value_code, MaxValueType):\n            value_code = value_code.value\n        return value_code\n\n    def get_max_value_data(self) -> dict[str, Any]:\n        file_path = core.Path(\"max_values.json\", True)\n        if not file_path.exists():\n            return {}\n        try:\n            return core.JsonFile.from_data(file_path.read()).to_object()\n        except core.JSONDecodeError:\n            return {}\n\n    def get(self, value_code: str | MaxValueType) -> int:\n        try:\n            return int(self.max_value_data.get(self.convert_val_code(value_code), 0))\n        except ValueError:\n            return 0\n\n    def get_property(self, value_code: str | MaxValueType, property: str) -> int:\n        try:\n            return int(\n                self.max_value_data.get(self.convert_val_code(value_code), {}).get(\n                    property, 0\n                )\n            )\n        except ValueError:\n            return 0\n\n    def get_old(self, value_code: str | MaxValueType) -> int:\n        return self.get_property(value_code, \"old\")\n\n    def get_new(self, value_code: str | MaxValueType) -> int:\n        return self.get_property(value_code, \"new\")\n"
  },
  {
    "path": "src/bcsfe/core/server/__init__.py",
    "content": "from bcsfe.core.server import (\n    managed_item,\n    headers,\n    client_info,\n    server_handler,\n    game_data_getter,\n    request,\n    updater,\n    event_data,\n)\n\n__all__ = [\n    \"managed_item\",\n    \"server_handler\",\n    \"headers\",\n    \"client_info\",\n    \"game_data_getter\",\n    \"request\",\n    \"updater\",\n    \"event_data\"\n]\n"
  },
  {
    "path": "src/bcsfe/core/server/client_info.py",
    "content": "from __future__ import annotations\nfrom typing import Any\nfrom bcsfe import core\n\n\nclass ClientInfo:\n    def __init__(self, cc: core.CountryCode, gv: core.GameVersion):\n        self.cc = cc\n        self.gv = gv\n\n    @staticmethod\n    def from_save_file(save_file: core.SaveFile):\n        return ClientInfo(save_file.cc, save_file.game_version)\n\n    def get_client_info(self) -> dict[str, Any]:\n        cc = self.cc.get_client_info_code()\n\n        data = {\n            \"clientInfo\": {\n                \"client\": {\n                    \"countryCode\": cc,\n                    \"version\": self.gv.game_version,\n                },\n                \"device\": {\n                    \"model\": \"SM-G955F\",\n                },\n                \"os\": {\n                    \"type\": \"android\",\n                    \"version\": \"9\",\n                },\n            },\n            \"nonce\": core.Random.get_hex_string(32),\n        }\n        return data\n"
  },
  {
    "path": "src/bcsfe/core/server/event_data.py",
    "content": "from __future__ import annotations\nfrom collections.abc import Callable\nfrom typing import Type, TypeVar\n\nfrom bcsfe import core\n\n\nclass FilterDate:\n    def __init__(self, start_mmdd: int, start_hhmm: int, end_mmdd: int, end_hhmm: int):\n        self.start_mmdd = start_mmdd\n        self.start_hhmm = start_hhmm\n        self.end_mmdd = end_mmdd\n        self.end_hhmm = end_hhmm\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> FilterDate:\n        return FilterDate(\n            row.next_int(), row.next_int(), row.next_int(), row.next_int()\n        )\n\n\nclass FilterItem:\n    def __init__(\n        self,\n        filter_date: FilterDate | None,\n        filter_day_flags: list[bool],  # 31 item array\n        filter_week: int,\n        filter_times_start_end_hhmm: list[tuple[int, int]],\n    ):\n        self.filter_date = filter_date\n        self.filter_day_flags = filter_day_flags\n        self.filter_week = filter_week\n        self.filter_times_start_end_hhmm = filter_times_start_end_hhmm\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> FilterItem:\n        filter_date_enabled = row.next_bool()\n\n        filter_date = None\n        if filter_date_enabled:\n            filter_date = FilterDate.from_csv_row(row)\n\n        filter_day_count = row.next_int()\n\n        filter_day_flags: list[bool] = [False] * 31\n\n        for _ in range(filter_day_count):\n            day_ind = row.next_int() - 1\n            if day_ind >= 0 and day_ind < len(filter_day_flags):\n                filter_day_flags[day_ind] = True\n\n        filter_week = row.next_int()\n        filter_time_count = row.next_int()\n\n        filter_times_start_end_hhmm: list[tuple[int, int]] = []\n\n        for _ in range(filter_time_count):\n            start_hhmm = row.next_int()\n            end_hhmm = row.next_int()\n\n            filter_times_start_end_hhmm.append((start_hhmm, end_hhmm))\n\n        return FilterItem(\n            filter_date, filter_day_flags, filter_week, filter_times_start_end_hhmm\n        )\n\n\ndef split_yyyymmdd(yyyymmdd: int) -> tuple[int, int, int]:\n    year = yyyymmdd // 10_000\n    month = (yyyymmdd % 10_000) // 100\n    day = yyyymmdd % 100\n\n    return year, month, day\n\n\ndef split_hhmm(hhmm: int) -> tuple[int, int]:\n    hour = hhmm // 100\n    minute = hhmm % 100\n\n    return hour, minute\n\n\nclass FilterData:\n    def __init__(\n        self,\n        start_yyyymmdd: int,\n        start_hhmm: int,\n        end_yyyymmdd: int,\n        end_hhmm: int,\n        min_game_version: int,\n        max_game_version: int,\n        platform_flag: int,\n        filter_items: list[FilterItem],\n    ):\n        self.start_yyyymmdd = start_yyyymmdd\n        self.start_hhmm = start_hhmm\n        self.end_yyyymmdd = end_yyyymmdd\n        self.end_hhmm = end_hhmm\n        self.min_game_version = min_game_version\n        self.max_game_version = max_game_version\n        self.platform_flag = platform_flag\n        self.filter_items = filter_items\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> FilterData:\n        start_yyyymmdd = row.next_int()\n        start_hhmm = row.next_int()\n        end_yyyymmdd = row.next_int()\n        end_hhmm = row.next_int()\n        min_game_version = row.next_int()\n        max_game_version = row.next_int()\n        platform_flag = row.next_int()\n        total_items = row.next_int()\n\n        filter_items: list[FilterItem] = []\n\n        for _ in range(total_items):\n            filter_items.append(FilterItem.from_csv_row(row))\n\n        return FilterData(\n            start_yyyymmdd,\n            start_hhmm,\n            end_yyyymmdd,\n            end_hhmm,\n            min_game_version,\n            max_game_version,\n            platform_flag,\n            filter_items,\n        )\n\n\nclass Localization:\n    def __init__(self, lang: str, title: str, message: str):\n        self.lang = lang\n        self.title = title\n        self.message = message\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> Localization:\n        return Localization(row.next_str(), row.next_str(), row.next_str())\n\n\nclass RarityGatya:\n    def __init__(self, prob: int, guaranteed: int):\n        self.prob = prob\n        self.guaranteed = guaranteed\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> RarityGatya:\n        return RarityGatya(row.next_int(), row.next_int())\n\n\nclass ServerGatyaDataSet:\n    def __init__(\n        self,\n        number: int,\n        catfood: int,\n        stage_progress: int,\n        flags: int,\n        rarity_info: list[RarityGatya],\n        message: str,\n        collab_message: tuple[str, str] | None,\n    ):\n        self.number = number\n        self.catfood = catfood\n        self.stage_progress = stage_progress\n        self.flags = flags\n        self.rarity_info = rarity_info\n        self.message = message\n        self.other_event_message = collab_message\n\n    @staticmethod\n    def from_csv_row(row: core.Row, flag: int) -> ServerGatyaDataSet:\n        number = row.next_int()\n        catfood = row.next_int()\n        stage_progress = row.next_int()\n        flags = row.next_int()\n        rarity_info: list[RarityGatya] = []\n\n        for _ in range(5):\n            rarity_info.append(RarityGatya.from_csv_row(row))\n\n        message = row.next_str()\n\n        collab_message = None\n        if flag == 4:\n            collab_message = (row.next_str(), row.next_str())\n\n        return ServerGatyaDataSet(\n            number,\n            catfood,\n            stage_progress,\n            flags,\n            rarity_info,\n            message,\n            collab_message,\n        )\n\n    def is_visible_silhouette(self) -> bool:\n        return (self.flags & 1) != 0\n\n    def is_required_user_rank_1600(self) -> bool:\n        return (self.flags & 2) != 0\n\n    def has_stepup_gatya(self) -> bool:\n        return (self.flags & 4) != 0\n\n\nclass ServerGatyaDataItem:\n    def __init__(self, filter: FilterData, flags: int, sets: list[ServerGatyaDataSet]):\n        self.filter = filter\n        self.flags = flags\n        self.sets = sets\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> ServerGatyaDataItem:\n        filter = FilterData.from_csv_row(row)\n        flag = row.next_int()\n        count = row.next_int()\n\n        sets: list[ServerGatyaDataSet] = []\n\n        for _ in range(count):\n            sets.append(ServerGatyaDataSet.from_csv_row(row, flag))\n\n        return ServerGatyaDataItem(filter, flag, sets)\n\n    def get_normal_flag(self) -> bool:\n        return self.flags == 0\n\n    def get_rare_flag(self) -> bool:\n        return 1 <= self.flags <= 3\n\n    def get_collab_flag(self) -> bool:\n        return self.flags == 4\n\n    def get_first_rare_flag(self) -> bool:\n        return self.flags == 2\n\n    def get_first_rare_10_flag(self) -> bool:\n        return self.flags == 3\n\n\nclass ServerItemDataItem:\n    def __init__(\n        self,\n        filter: FilterData,\n        event_number: int,  # server item id\n        item_number: int,\n        item_unit: int,  # base quanity, not cat unit (e.g 2 XP+1000s)\n        title: str,\n        message: str,\n        stage_progress: int,\n        stage_progress_flag: int,\n        flags: int,\n        locales: list[Localization] | None,\n    ):\n        self.filter = filter\n        self.event_number = event_number\n        self.item_number = item_number\n        self.item_unit = item_unit\n        self.title = title\n        self.message = message\n        self.stage_progress = stage_progress\n        self.stage_progress_flag = stage_progress_flag\n        self.flags = flags\n        self.locales = locales\n\n    def is_every_day(self) -> bool:\n        return (self.flags & 1) != 0\n\n    def is_required_user_rank_1600(self) -> bool:\n        return (self.flags & 2) != 0\n\n    @staticmethod\n    def from_csv_row(row: core.Row) -> ServerItemDataItem:\n        filter = FilterData.from_csv_row(row)\n\n        event_number = row.next_int()\n        item_number = row.next_int()\n        item_unit = row.next_int()\n        title = row.next_str()\n        message = row.next_str()\n        stage_progress = row.next_int()\n        stage_progress_flag = row.next_bool()\n        flags = row.next_int()\n\n        locales: list[Localization] | None = None\n\n        if not row.done():\n            locales = []\n            total_locales = row.next_int()\n\n            for _ in range(total_locales):\n                locales.append(Localization.from_csv_row(row))\n\n        return ServerItemDataItem(\n            filter,\n            event_number,\n            item_number,\n            item_unit,\n            title,\n            message,\n            stage_progress,\n            stage_progress_flag,\n            flags,\n            locales,\n        )\n\n\nItem = TypeVar(\"Item\")\nT = TypeVar(\"T\")\n\n\ndef read_event_data(\n    csv: core.CSV,\n    read_func: Callable[[core.Row], Item],\n    init_func: Callable[[list[Item]], T],\n) -> T | None:\n    start = csv.read_line()\n    if start is None:\n        return None\n\n    if start.next_str() != \"[start]\":\n        return None\n\n    if not start.done():\n        return None\n\n    items: list[Item] = []\n\n    while True:\n        row = csv.read_line()\n        if row is None:\n            return None\n\n        if len(row) == 0:\n            return None\n\n        if row[0].to_str() == \"[end]\":\n            break\n\n        item = read_func(row)\n\n        items.append(item)\n\n    return init_func(items)\n\n\nclass ServerItemData:\n    def __init__(self, items: list[ServerItemDataItem]):\n        self.items = items\n\n    @staticmethod\n    def from_csv(csv: core.CSV) -> ServerItemData | None:\n        return read_event_data(csv, ServerItemDataItem.from_csv_row, ServerItemData)\n\n    @staticmethod\n    def from_data(data: core.Data) -> ServerItemData | None:\n        csv = core.CSV(data, delimiter=\"\\t\", remove_comments=False, remove_empty=False)\n\n        return ServerItemData.from_csv(csv)\n\n\nclass ServerGatyaData:\n    def __init__(self, items: list[ServerGatyaDataItem]):\n        self.items = items\n\n    @staticmethod\n    def from_csv(csv: core.CSV) -> ServerGatyaData | None:\n        return read_event_data(csv, ServerGatyaDataItem.from_csv_row, ServerGatyaData)\n\n    @staticmethod\n    def from_data(data: core.Data) -> ServerGatyaData | None:\n        csv = core.CSV(data, delimiter=\"\\t\", remove_comments=False, remove_empty=False)\n\n        return ServerGatyaData.from_csv(csv)\n"
  },
  {
    "path": "src/bcsfe/core/server/game_data_getter.py",
    "content": "from __future__ import annotations\nfrom io import BytesIO\nfrom typing import Any, Callable\n\nfrom bcsfe.cli import color, dialog_creator\n\nimport tarfile\n\nfrom bcsfe import core\n\n\nclass GameDataGetter:\n    def __init__(\n        self, cc: core.CountryCode, gv: core.GameVersion, do_print: bool = True\n    ):\n        self.repo_url = core.core_data.config.get_game_data_repo()\n        self.print = do_print\n        self.lang = core.core_data.config.get_str(core.ConfigKey.LOCALE)\n        self.cc = cc.get_cc_lang()\n        self.real_cc = cc\n        self.gv = gv\n        self.cc = self.cc if not self.cc.is_lang() else self.real_cc\n        self.version, exact_match = self.find_gv(self.cc, gv)\n\n        self.all_versions = None\n        self.url = None\n        self.filepath = None\n\n        if exact_match:\n            return\n\n        self.metadata = self.get_metadata()\n        if self.metadata is None:\n            return\n        self.all_versions = self.get_versions(self.metadata)\n        self.url = self.metadata.get(\"base_url\")\n        if self.all_versions is not None:\n            self.version, self.filepath = self.get_version(self.all_versions, self.cc)\n\n    def find_gv(\n        self, cc: core.CountryCode, gv: core.GameVersion\n    ) -> tuple[str | None, bool]:\n        versions = GameDataGetter.get_all_downloaded_versions().get(cc.get_code())\n        if versions is None:\n            return None, False\n\n        versions_int = [\n            core.GameVersion.from_string(version).game_version for version in versions\n        ]\n\n        versions_int.sort()\n\n        for version in versions_int:\n            if version >= gv.game_version:\n                return core.GameVersion(version).to_string(), version == gv.game_version\n        return None, False\n\n    def does_save_version_match(self, save_file: core.SaveFile) -> bool:\n        if self.version is None:\n            return False\n\n        return save_file.game_version == self.version\n\n    def get_version(\n        self, versions: dict[str, dict[str, str]], cc: core.CountryCode\n    ) -> tuple[str | None, str | None]:\n        cc_versions = versions.get(cc.get_code())\n        if cc_versions is None:\n            return None, None\n        if not cc_versions:\n            return None, None\n        gv_string = self.gv.to_string()\n        if gv_string not in cc_versions:\n            cc_version_keys = list(cc_versions.keys())\n            cc_version_keys.sort()\n            for version in cc_version_keys:\n                if (\n                    core.GameVersion.from_string(version).game_version\n                    >= self.gv.game_version\n                ):\n                    return version, cc_versions[version]\n            return cc_version_keys[-1], cc_versions[cc_version_keys[-1]]\n        return gv_string, cc_versions[gv_string]\n\n    def get_metadata(self, show_alt: bool = True) -> dict[str, Any] | None:\n        response = core.RequestHandler(self.repo_url).get()\n        if response is None:\n            if (\n                self.repo_url\n                == core.core_data.config.get_default(core.ConfigKey.GAME_DATA_REPO)\n                and show_alt\n            ):\n                alt = \"https://gitlab.com/fieryhenry/bcdata/-/raw/main/metadata.json\"\n                res = dialog_creator.YesNoInput().get_input_once(\n                    \"use_alternative_repo\",\n                    {\"repo\": alt},\n                )\n                if res:\n                    core.core_data.config.set(core.ConfigKey.GAME_DATA_REPO, alt)\n                    self.repo_url = alt\n                    return self.get_metadata(show_alt=False)\n\n            return None\n        try:\n            data = response.json()\n        except core.JSONDecodeError as e:\n            print(e, f\"Data:\\n{response.text}\")\n            return None\n        return data\n\n    def get_versions(self, metdata: dict[str, Any]) -> dict[str, dict[str, str]] | None:\n        return metdata.get(\"versions\")\n\n    def get_packname(self, packname: str) -> str:\n        if packname != \"resLocal\":\n            return packname\n        if self.cc != core.CountryCodeType.EN:\n            return packname\n        langs = core.CountryCode.get_langs()\n        if self.lang in langs:\n            return f\"{packname}_{self.lang}\"\n        return packname\n\n    @staticmethod\n    def get_game_data_dir() -> core.Path:\n        path = core.get_game_data_path()\n        if path is None:\n            return core.Path.get_data_folder().add(\"game_data\")\n        return path\n\n    def get_file_path(self, pack_name: str, file_name: str) -> core.Path | None:\n        pack_name = self.get_packname(pack_name)\n        path = self.get_version_path()\n        if path is None:\n            return None\n        return path.add(pack_name).generate_dirs().add(file_name)\n\n    def download_version_data(self):\n        if self.url is None or self.filepath is None or self.version is None:\n            return None\n        url = self.url + self.filepath\n\n        if self.print:\n            color.ColoredText.localize(\"downloading_compressed_data\", url=url)\n\n        downloaded_data = core.RequestHandler(url).get()\n        if downloaded_data is None:\n            if self.print:\n                color.ColoredText.localize(\"no_internet\")\n            return None\n\n        archive = tarfile.open(\n            name=self.filepath, fileobj=BytesIO(downloaded_data.content)\n        )\n\n        outdir = (\n            GameDataGetter.get_game_data_dir().add(self.cc.get_code()).add(self.version)\n        ).generate_dirs()\n\n        archive.extractall(outdir.path)\n\n        outdir.add(\"downloaded\").write(core.Data())\n\n        return True\n\n    def get_version_path(self) -> core.Path | None:\n        if self.version is None:\n            return None\n        return (\n            GameDataGetter.get_game_data_dir().add(self.cc.get_code()).add(self.version)\n        ).generate_dirs()\n\n    def has_downloaded(self) -> bool:\n        path = self.get_version_path()\n        if path is None:\n            return False\n        return path.add(\"downloaded\").exists()\n\n    def get_file(self, pack_name: str, file_name: str) -> core.Data | bool:\n        path = self.get_file_path(pack_name, file_name)\n        if path is None:\n            return False\n\n        if path.exists():\n            return path.read()\n        else:\n            if self.has_downloaded():\n                return True\n            if self.download_version_data() is None:\n                return False\n\n            path = self.get_file_path(pack_name, file_name)\n            if path is None:\n                return False\n\n            if path.exists():\n                return path.read()\n            return self.has_downloaded()\n\n    def save_file(self, pack_name: str, file_name: str) -> core.Data | bool:\n        pack_name = self.get_packname(pack_name)\n        data = self.get_file(pack_name, file_name)\n        if isinstance(data, bool):\n            return data\n\n        path = self.get_file_path(pack_name, file_name)\n        if path is None:\n            return False\n        data.to_file(path)\n        return data\n\n    def save_file_data(\n        self, pack_name: str, file_name: str, data: core.Data\n    ) -> core.Data | None:\n        pack_name = self.get_packname(pack_name)\n\n        path = self.get_file_path(pack_name, file_name)\n        if path is None:\n            return None\n        data.to_file(path)\n        return data\n\n    def is_downloaded(self, pack_name: str, file_name: str) -> bool:\n        pack_name = self.get_packname(pack_name)\n        path = self.get_file_path(pack_name, file_name)\n        if path is None:\n            return False\n        return path.exists()\n\n    def download_from_path(\n        self, path: str, retries: int = 2, display_text: bool = True\n    ) -> core.Data | None:\n        pack_name, file_name = path.split(\"/\")\n        pack_name = self.get_packname(pack_name)\n        return self.download(pack_name, file_name, retries, display_text)\n\n    def download(\n        self,\n        pack_name: str,\n        file_name: str,\n        retries: int = 2,\n        display_text: bool = True,\n    ) -> core.Data | None:\n        retries -= 1\n        pack_name = self.get_packname(pack_name)\n\n        if self.is_downloaded(pack_name, file_name):\n            path = self.get_file_path(pack_name, file_name)\n            if path is None:\n                return None\n            try:\n                return path.read()\n            except FileNotFoundError:\n                return None\n\n        if retries == 0:\n            return None\n\n        version = self.version\n\n        if version is None:\n            if display_text:\n                self.print_no_file(pack_name, file_name)\n            return None\n\n        if display_text and not self.has_downloaded():\n            color.ColoredText.localize(\n                \"downloading\",\n                file_name=file_name,\n                pack_name=pack_name,\n                country_code=self.cc.get_code(),\n                version=version,\n            )\n        data = self.save_file(pack_name, file_name)\n        if isinstance(data, bool):\n            if not data and display_text:\n                self.print_no_file(pack_name, file_name)\n            return None\n\n        data = self.download(pack_name, file_name, retries, display_text)\n        if data is None:\n            if display_text:\n                self.print_no_file(pack_name, file_name)\n            return None\n        return data\n\n    def download_all(\n        self,\n        pack_name: str,\n        file_names: list[str],\n        display_text: bool = True,\n    ) -> list[tuple[str, core.Data] | None]:\n        pack_name = self.get_packname(pack_name)\n\n        callables: list[Callable[..., Any]] = []\n        args: list[tuple[str, str, int, bool]] = []\n        for file_name in file_names:\n            callables.append(self.download)\n            args.append((pack_name, file_name, 2, display_text))\n        core.thread_run_many(callables, args)\n        data_list: list[tuple[str, core.Data] | None] = []\n        for file_name in file_names:\n            path = self.get_file_path(pack_name, file_name)\n            if path is None:\n                data_list.append(None)\n            elif not path.exists():\n                data_list.append(None)\n            else:\n                data_list.append((file_name, path.read()))\n        return data_list\n\n    @staticmethod\n    def get_all_downloaded_versions() -> dict[str, list[str]]:\n        versions: dict[str, list[str]] = {}\n        for cc in core.CountryCode.get_all_str():\n            dir = GameDataGetter.get_game_data_dir().add(cc)\n            if not dir.exists():\n                continue\n            for version in GameDataGetter.get_game_data_dir().add(cc).get_dirs():\n                if not version.exists():\n                    continue\n                if not version.add(\"downloaded\").exists():\n                    continue\n                if cc in versions:\n                    versions[cc].append(version.basename())\n                else:\n                    versions[cc] = [version.basename()]\n\n        return versions\n\n    @staticmethod\n    def delete_old_versions(to_keep: int) -> None:\n        versions = GameDataGetter.get_all_downloaded_versions()\n        for cc, cc_versions in versions.items():\n            cc_versions.sort(reverse=True)\n            to_keep = min(to_keep, len(cc_versions))\n            for version in cc_versions[to_keep:]:\n                path = GameDataGetter.get_game_data_dir().add(cc).add(version)\n                path.remove()\n\n    def print_no_file(self, packname: str, file_name: str) -> None:\n        if self.version is None:\n            color.ColoredText.localize(\"failed_to_get_game_versions\")\n        else:\n            color.ColoredText.localize(\n                \"failed_to_download_game_data\",\n                file_name=file_name,\n                pack_name=packname,\n                country_code=self.cc.get_code(),\n                version=self.version,\n                url=self.url,\n            )\n"
  },
  {
    "path": "src/bcsfe/core/server/headers.py",
    "content": "from __future__ import annotations\nimport time\nfrom bcsfe import core\n\n\nclass AccountHeaders:\n    def __init__(self, save_file: core.SaveFile, data: str):\n        self.save_file = save_file\n        self.data = data\n\n    def get_headers(self) -> dict[str, str]:\n        return AccountHeaders.get_headers_static(\n            self.save_file.inquiry_code, self.data\n        )\n\n    @staticmethod\n    def get_headers_static(iq: str, data: str):\n        return {\n            \"accept-enconding\": \"gzip\",\n            \"connection\": \"keep-alive\",\n            \"content-type\": \"application/json\",\n            \"nyanko-signature\": core.NyankoSignature(\n                iq, data\n            ).generate_signature(),\n            \"nyanko-timestamp\": str(int(time.time())),\n            \"nyanko-signature-version\": \"1\",\n            \"nyanko-signature-algorithm\": \"HMACSHA256\",\n            \"user-agent\": \"Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)\",\n        }\n"
  },
  {
    "path": "src/bcsfe/core/server/managed_item.py",
    "content": "from __future__ import annotations\n\n\"\"\"ManagedItem class for bcsfe.\"\"\"\n\nfrom enum import Enum\nfrom typing import Any\nimport uuid\nimport time\nfrom bcsfe import core\n\n\nclass DetailType(Enum):\n    \"\"\"Enum for the different types of details.\"\"\"\n\n    GET = \"get\"\n    USE = \"use\"\n\n\nclass ManagedItemType(Enum):\n    \"\"\"Enum for the different types of managed items.\"\"\"\n\n    CATFOOD = \"catfood\"\n    RARE_TICKET = \"rareTicket\"\n    PLATINUM_TICKET = \"platinumTicket\"\n    LEGEND_TICKET = \"legendTicket\"\n\n\nclass ManagedItem:\n    \"\"\"Managed item for backupmetadata\"\"\"\n\n    def __init__(\n        self,\n        amount: int,\n        detail_type: DetailType,\n        managed_item_type: ManagedItemType,\n        detail_code: str = \"\",\n        detail_created_at: int = 0,\n    ):\n        self.amount = amount\n        self.detail_type = detail_type\n        self.managed_item_type = managed_item_type\n        if not detail_code:\n            detail_code = str(uuid.uuid4())\n        self.detail_code = detail_code\n        if not detail_created_at:\n            detail_created_at = int(time.time())\n        self.detail_created_at = detail_created_at\n\n    @staticmethod\n    def from_change(\n        change: int, managed_item_type: ManagedItemType\n    ) -> ManagedItem:\n        \"\"\"Create a managed item from a change.\"\"\"\n        if change > 0:\n            detail_type = DetailType.GET\n        else:\n            detail_type = DetailType.USE\n        managed_item = ManagedItem(abs(change), detail_type, managed_item_type)\n        return managed_item\n\n    def to_dict(self) -> dict[str, Any]:\n        \"\"\"Convert the managed item to a dictionary.\"\"\"\n\n        data = {\n            \"amount\": self.amount,\n            \"detailCode\": self.detail_code,\n            \"detailCreatedAt\": self.detail_created_at,\n            \"detailType\": self.detail_type.value,\n            \"managedItemType\": self.managed_item_type.value,\n        }\n        return data\n\n    def to_short_form(self) -> str:\n        \"\"\"Convert the managed item to a short form.\"\"\"\n\n        return f\"{self.amount}_{self.detail_created_at}_{self.managed_item_type.value}_{self.detail_type.value}\"\n\n    @staticmethod\n    def from_short_form(short_form: str) -> ManagedItem:\n        values = short_form.split(\"_\")\n        try:\n            amount = int(values[0])\n        except (IndexError, ValueError):\n            amount = 0\n\n        try:\n            detail_created_at = int(values[1])\n        except (IndexError, ValueError):\n            detail_created_at = 0\n\n        try:\n            managed_item_type = values[2]\n        except IndexError:\n            managed_item_type = ManagedItemType.CATFOOD.value\n\n        try:\n            detail_type = values[3]\n        except IndexError:\n            detail_type = DetailType.GET.value\n\n        return ManagedItem(\n            amount,\n            DetailType(detail_type),\n            ManagedItemType(managed_item_type),\n            detail_created_at=detail_created_at,\n        )\n\n    def __str__(self) -> str:\n        return f\"{self.amount} {self.managed_item_type.value} ({self.detail_type.value})\"\n\n    def __repr__(self) -> str:\n        return f\"{self.amount} {self.managed_item_type.value} ({self.detail_type.value})\"\n\n\nclass BackupMetaData:\n    def __init__(\n        self,\n        save_file: core.SaveFile,\n    ):\n        self.save_file = save_file\n        self.identifier = \"managed_items\"\n\n    def set_managed_items(self, managed_items: list[ManagedItem]):\n        self.save_file.remove_strings(self.identifier)\n        for managed_item in managed_items:\n            string = managed_item.to_short_form()\n            self.save_file.store_string(\n                self.identifier, string, overwrite=False\n            )\n\n    def get_managed_items(self) -> list[ManagedItem]:\n        managed_items: list[ManagedItem] = []\n        managed_items_str = self.save_file.get_strings(self.identifier)\n        for managed_item_str in managed_items_str:\n            managed_item = ManagedItem.from_short_form(managed_item_str)\n            if managed_item.amount == 0:\n                continue\n            managed_items.append(managed_item)\n        return managed_items\n\n    def add_managed_item(self, managed_item: ManagedItem):\n        if managed_item.amount == 0:\n            return\n        managed_items = self.get_managed_items()\n        managed_items.append(managed_item)\n        self.set_managed_items(managed_items)\n\n    def remove_managed_items(self) -> None:\n        self.save_file.remove_strings(self.identifier)\n\n    def create(\n        self, save_key: str | None = None, add_managed_items: bool = True\n    ) -> str:\n        \"\"\"Create the backup metadata.\"\"\"\n\n        return BackupMetaData.create_static(\n            self.save_file.inquiry_code,\n            self.save_file.officer_pass.play_time,\n            self.save_file.calculate_user_rank(),\n            self.get_managed_items(),\n            save_key,\n            add_managed_items,\n        )\n\n    @staticmethod\n    def create_static(\n        iq: str,\n        playtime: int,\n        userrank: int,\n        items: list[ManagedItem],\n        save_key: str | None = None,\n        add_managed_items: bool = True,\n    ):\n        managed_items: list[dict[str, Any]] = []\n        if add_managed_items:\n            for managed_item in items:\n                if managed_item.amount == 0:\n                    continue\n                managed_items.append(managed_item.to_dict())\n\n        managed_items_json = core.JsonFile.from_object(managed_items)\n        managed_items_str = (\n            managed_items_json.to_data(indent=None).to_str().replace(\" \", \"\")\n        )\n\n        backup_metadata: dict[str, Any] = {\n            \"managedItemDetails\": managed_items,\n            \"nonce\": core.Random.get_hex_string(32),\n            \"playTime\": playtime,\n            \"rank\": userrank,\n            \"receiptLogIds\": [],\n            \"signature_v1\": core.NyankoSignature(\n                iq, managed_items_str\n            ).generate_signature_v1(),\n        }\n        if save_key is not None:\n            backup_metadata[\"saveKey\"] = save_key\n        return (\n            core.JsonFile.from_object(backup_metadata)\n            .to_data(indent=None)\n            .to_str()\n            .replace(\" \", \"\")\n        )\n"
  },
  {
    "path": "src/bcsfe/core/server/request.py",
    "content": "from __future__ import annotations\n\nimport requests\n\nfrom bcsfe import core\n\n\nclass MultiPartFile:\n    def __init__(self, content: bytes, content_type: str, filename: str | None = None):\n        self.content = content\n        self.content_type = content_type\n        self.filename = filename\n\n\nclass MultipartForm:\n    def __init__(self):\n        self.data: dict[str, MultiPartFile] = {}\n\n    def into_files(\n        self,\n    ) -> dict[str, tuple[str | None, bytes, str]]:\n        out = {}\n        for name, data in self.data.items():\n            out[name] = (data.filename, data.content, data.content_type)\n\n        return out\n\n    def add_key(\n        self, key: str, content: bytes, content_type: str, filename: str | None = None\n    ):\n        self.data[key] = MultiPartFile(content, content_type, filename)\n\n    def get_all_type(self, content_type: str) -> str:\n        data = \"\"\n        for key, file in self.data.items():\n            if file.content_type == content_type:\n                content = file.content.decode(\"utf-8\", errors=\"ignore\")\n                data += f\"key: {key}, data: {content}\\n\"\n\n        return data\n\n\nclass RequestHandler:\n    \"\"\"Handles HTTP requests.\"\"\"\n\n    def __init__(\n        self,\n        url: str,\n        headers: dict[str, str] | None = None,\n        data: core.Data | None = None,\n        form: MultipartForm | None = None,\n    ):\n        \"\"\"Initializes a new instance of the RequestHandler class.\n\n        Args:\n            url (str): URL to request.\n            headers (dict[str, str] | None, optional): Headers to send with the request. Defaults to None.\n            data (core.Data | None, optional): Data to send with the request. Defaults to None.\n        \"\"\"\n        if data is None:\n            data = core.Data()\n        self.url = url\n        self.headers = headers\n        self.data = data\n        self.form = form\n\n    def get(\n        self,\n        stream: bool = False,\n        no_timeout: bool = False,\n    ) -> requests.Response | None:\n        \"\"\"Sends a GET request.\n\n        Returns:\n            requests.Response: Response from the server.\n        \"\"\"\n        try:\n            return requests.get(\n                self.url,\n                headers=self.headers,\n                timeout=(\n                    None\n                    if no_timeout\n                    else core.core_data.config.get_int(\n                        core.ConfigKey.MAX_REQUEST_TIMEOUT\n                    )\n                ),\n                stream=stream,\n                files=None if self.form is None else self.form.into_files(),\n            )\n        except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):\n            return None\n\n    def post(self, no_timeout: bool = False) -> requests.Response | None:\n        \"\"\"Sends a POST request.\n\n        Returns:\n            requests.Response: Response from the server.\n        \"\"\"\n        try:\n            return requests.post(\n                self.url,\n                headers=self.headers,\n                data=self.data.data,\n                timeout=(\n                    None\n                    if no_timeout\n                    else core.core_data.config.get_int(\n                        core.ConfigKey.MAX_REQUEST_TIMEOUT\n                    )\n                ),\n                files=None if self.form is None else self.form.into_files(),\n            )\n        except requests.exceptions.ConnectionError:\n            return None\n"
  },
  {
    "path": "src/bcsfe/core/server/server_handler.py",
    "content": "from __future__ import annotations\nimport base64\nimport time\nfrom typing import Any\n\nfrom bcsfe import core\nimport jwt\n\nfrom bcsfe.cli import color\n\n\nclass RequestResult:\n    def __init__(\n        self,\n        url: str,\n        response: core.Response | None,\n        headers: dict[str, str],\n        data: str,\n        payload: dict[str, Any] | None = None,\n        timestamp: str | None = None,\n    ):\n        self.url = url\n        self.response = response\n        self.headers = headers\n        self.data = data\n        self.payload = payload\n        self.timestamp = timestamp\n\n\nclass ServerHandler:\n    auth_url = \"https://nyanko-auth.ponosgames.com\"\n    save_url = \"https://nyanko-save.ponosgames.com\"\n    backups_url = \"https://nyanko-backups.ponosgames.com\"\n    aws_url = \"https://nyanko-service-data-prd.s3.amazonaws.com\"\n    managed_item_url = \"https://nyanko-managed-item.ponosgames.com\"\n    events_url = \"https://nyanko-events.ponosgames.com\"\n\n    def __init__(self, save_file: core.SaveFile, print: bool = True):\n        self.save_file = save_file\n        self.print = print\n        self.counter = 0\n\n    @staticmethod\n    def get_password_key() -> str:\n        return \"password\"\n\n    @staticmethod\n    def get_auth_token_key() -> str:\n        return \"auth_token\"\n\n    @staticmethod\n    def get_save_key_key() -> str:\n        return \"save_key\"\n\n    def save_password(self, password: str):\n        self.save_file.store_string(ServerHandler.get_password_key(), password)\n\n    def get_stored_password(self) -> str | None:\n        return self.save_file.get_string(ServerHandler.get_password_key())\n\n    def remove_stored_password(self):\n        self.save_file.remove_string(ServerHandler.get_password_key())\n\n    def save_save_key_data(self, save_key: dict[str, Any]):\n        self.save_file.store_dict(ServerHandler.get_save_key_key(), save_key)\n\n    def get_stored_save_key_data(self) -> dict[str, Any] | None:\n        save_key_data = self.save_file.get_dict(ServerHandler.get_save_key_key())\n        if save_key_data is None:\n            return None\n        if not self.validate_save_key_data(save_key_data):\n            self.remove_stored_save_key_data()\n            return None\n        return save_key_data\n\n    def validate_save_key_data(self, save_key_data: dict[str, Any]) -> bool:\n        key = save_key_data.get(\"key\")\n        if key is None:\n            return False\n        if key.split(\"/\")[2] != self.save_file.inquiry_code:\n            return False\n        policy = save_key_data.get(\"policy\")\n        if policy is None:\n            return False\n        policy = base64.b64decode(policy)\n        json_policy = core.JsonFile.from_data(core.Data(policy)).to_object()\n        expiration = json_policy.get(\"expiration\")\n        if expiration is None:\n            return False\n        expiration = int(\n            time.mktime(time.strptime(expiration, \"%Y-%m-%dT%H:%M:%S.%fZ\"))\n        )\n        if expiration < time.time():\n            return False\n        return True\n\n    def remove_stored_save_key_data(self):\n        self.save_file.remove_dict(ServerHandler.get_save_key_key())\n\n    def save_auth_token(self, auth_token: str):\n        self.save_file.store_string(ServerHandler.get_auth_token_key(), auth_token)\n\n    def get_stored_auth_token(self) -> str | None:\n        token = self.save_file.get_string(ServerHandler.get_auth_token_key())\n        return token\n\n    def remove_stored_auth_token(self):\n        self.save_file.remove_string(ServerHandler.get_auth_token_key())\n\n    def get_password_new(self) -> str | None:\n        self.print_key(\"getting_password\")\n\n        url = f\"{self.auth_url}/v1/users\"\n        data = {\n            \"accountCode\": self.save_file.inquiry_code,\n            \"accountCreatedAt\": int(self.save_file.energy_penalty_timestamp),\n            \"nonce\": core.Random.get_hex_string(32),\n        }\n        password = self.do_password_request(url, data)\n        return password\n\n    @staticmethod\n    def log_error(key: str, result: RequestResult):\n        if \"EXPECT_THIS_TO_FAIL\" in result.data:\n            return\n        if result.response is None:\n            log_text = \"Failed to make request. Check your internet connection.\"\n            core.core_data.logger.log_error(log_text)\n            return\n        log_text = (\n            f\"Error: {key}\\n\"\n            f\"URL: {result.url}\\n\"\n            f\"Response Headers: {result.response.headers}\\n\"\n            f\"Response Body: {result.response.content.decode('utf-8')}\\n\"\n            f\"Status Code: {result.response.status_code}\\n\"\n            f\"Reason: {result.response.reason}\\n\"\n            f\"Request Headers: {result.headers}\\n\"\n            f\"Request Body: {result.data}\\n\"\n        )\n        core.core_data.logger.log_error(log_text)\n\n    def do_password_request(self, url: str, dict_data: dict[str, Any]) -> str | None:\n        result = self.do_request(url, dict_data)\n        if result.payload is None:\n            ServerHandler.log_error(\"password_fail\", result)\n            return None\n        payload = result.payload\n        password = payload.get(\"password\", None)\n        if password is None:\n            ServerHandler.log_error(\"password_fail\", result)\n            self.remove_stored_password()\n            return None\n        password_refresh_token = payload.get(\"passwordRefreshToken\", None)\n        if password_refresh_token is None:\n            ServerHandler.log_error(\"password_fail\", result)\n            self.remove_stored_password()\n            return None\n        account_code = payload.get(\"accountCode\", None)\n        timestamp = result.timestamp\n\n        self.save_file.password_refresh_token = password_refresh_token\n        self.save_password(password)\n        if account_code:\n            self.save_file.inquiry_code = account_code\n            self.remove_stored_auth_token()\n            self.remove_stored_save_key_data()\n\n            if timestamp is not None:\n                self.save_file.energy_penalty_timestamp = int(timestamp)\n            if not self.update_managed_items():\n                return None\n\n        return password\n\n    def do_request(self, url: str, dict_data: dict[str, Any]) -> RequestResult:\n        data = (\n            core.JsonFile.from_object(dict_data)\n            .to_data(indent=None)\n            .to_str()\n            .replace(\" \", \"\")\n        )\n        headers = core.AccountHeaders(self.save_file, data).get_headers()\n        response = core.RequestHandler(url, headers, core.Data(data)).post()\n        if response is None:\n            self.log_no_internet(RequestResult(url, None, headers, data))\n            return RequestResult(url, response, headers, data)\n        json: dict[str, Any] = response.json()\n        status_code = json.get(\"statusCode\", 0)\n        if status_code != 1:\n            return RequestResult(url, response, headers, data)\n\n        timestamp = json.get(\"timestamp\", None)\n\n        payload = json.get(\"payload\", {})\n        return RequestResult(url, response, headers, data, payload, timestamp)\n\n    def refresh_password(self) -> str | None:\n        self.print_key(\"refreshing_password\")\n\n        url = f\"{self.auth_url}/v1/user/password\"\n        data = {\n            \"accountCode\": self.save_file.inquiry_code,\n            \"passwordRefreshToken\": self.save_file.password_refresh_token,\n            \"nonce\": core.Random.get_hex_string(32),\n        }\n        return self.do_password_request(url, data)\n\n    def get_auth_token_new(self, password: str) -> str | None:\n        self.print_key(\"getting_auth_token\")\n\n        url = f\"{self.auth_url}/v1/tokens\"\n        data = core.ClientInfo.from_save_file(self.save_file).get_client_info()\n        data[\"password\"] = password\n        data[\"accountCode\"] = self.save_file.inquiry_code\n\n        result = self.do_request(url, data)\n        if result.payload is None:\n            ServerHandler.log_error(\"auth_token_fail\", result)\n            self.remove_stored_auth_token()\n            self.remove_stored_password()\n            return None\n        payload = result.payload\n        auth_token = payload.get(\"token\", None)\n        if auth_token is None:\n            ServerHandler.log_error(\"auth_token_fail\", result)\n            self.remove_stored_auth_token()\n            self.remove_stored_password()\n            return None\n        self.save_auth_token(auth_token)\n        return auth_token\n\n    def get_password(self, tries: int = 0) -> str | None:\n        password = self.get_stored_password()\n        if password is not None:\n            return password\n        password = self.refresh_password()\n        if password is not None:\n            return password\n        password = self.get_password_new()\n        if password is not None:\n            return password\n        self.create_new_account()\n        if tries >= 1:\n            return None\n        return self.get_password(tries + 1)\n\n    def validate_auth_token(self, auth_token: str) -> bool:\n        token = jwt.decode(  # type: ignore\n            auth_token,\n            algorithms=[\"HS256\"],\n            options={\"verify_signature\": False},\n        )\n        if not token:\n            return False\n        if token.get(\"exp\", 0) < time.time():\n            return False\n        if token.get(\"accountCode\", None) != self.save_file.inquiry_code:\n            return False\n\n        return True\n\n    def get_auth_token(self, tries: int = 1) -> str | None:\n        auth_token = self.get_stored_auth_token()\n        if auth_token is not None:\n            if self.validate_auth_token(auth_token):\n                return auth_token\n            self.remove_stored_auth_token()\n        password = self.get_password()\n        if password is None:\n            return None\n        auth_token = self.get_stored_auth_token()\n        if auth_token is not None:\n            return auth_token\n        auth_token = self.get_auth_token_new(password)\n        if auth_token is not None:\n            return auth_token\n\n        if tries > 0:\n            self.print_key(\"retry_auth_token\")\n            return self.get_auth_token(tries - 1)\n\n        return None\n\n    def log_no_internet(self, result: RequestResult):\n        ServerHandler.log_error(\"no_internet\", result)\n        if self.print:\n            core.print_no_internet()\n\n    def get_save_key_new(self, auth_token: str) -> dict[str, Any] | None:\n        self.print_key(\"getting_save_key\")\n\n        nonce = core.Random.get_hex_string(32)\n        url = f\"{self.save_url}/v2/save/key?nonce={nonce}\"\n        headers = {\n            \"accept-encoding\": \"gzip\",\n            \"connection\": \"keep-alive\",\n            \"authorization\": \"Bearer \" + auth_token,\n            \"nyanko-timestamp\": str(int(time.time())),\n            \"user-agent\": \"Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)\",\n        }\n        response = core.RequestHandler(url, headers).get()\n        if response is None:\n            self.log_no_internet(RequestResult(url, None, headers, \"\"))\n            return None\n        json: dict[str, Any] = response.json()\n        status_code = json.get(\"statusCode\", 0)\n        if status_code != 1:\n            ServerHandler.log_error(\n                \"save_key_fail\", RequestResult(url, response, headers, \"\")\n            )\n            self.remove_stored_auth_token()\n            return None\n        payload = json.get(\"payload\", {})\n        self.save_save_key_data(payload)\n        return payload\n\n    def get_save_key(self) -> dict[str, Any] | None:\n        # save_key = self.get_stored_save_key_data()\n        # if save_key and save_key.get(\"key\", None):\n        #    return save_key\n        auth_token = self.get_auth_token()\n        if auth_token is None:\n            return None\n        # save_key = self.get_stored_save_key_data()\n        # if save_key:\n        #    return save_key\n        save_key = self.get_save_key_new(auth_token)\n        if save_key is not None:\n            return save_key\n\n        return None\n\n    def get_upload_request_form(\n        self,\n        save_key: dict[str, str],\n    ) -> core.MultipartForm:\n        save_data = self.save_file.to_data()\n        form_data = core.MultipartForm()\n        for key, value in save_key.items():\n            if key == \"url\":\n                continue\n            form_data.add_key(key, value.encode(), \"text/plain\")\n\n        form_data.add_key(\n            \"file\", save_data.to_bytes(), \"application/octet-stream\", \"file.sav\"\n        )\n        return form_data\n\n    def upload_save_data(self, save_key: dict[str, Any]) -> bool:\n        self.print_key(\"uploading_save_file\")\n\n        form = self.get_upload_request_form(save_key)\n        if form is None:\n            self.remove_stored_save_key_data()\n            return False\n        url = save_key.get(\"url\")\n        if url is None:\n            url = f\"{self.aws_url}/\"\n        headers = {\n            \"accept-encoding\": \"gzip\",\n            \"connection\": \"keep-alive\",\n            \"user-agent\": \"Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)\",\n        }\n        response = core.RequestHandler(url, headers, form=form).post(no_timeout=True)\n        if response is None:\n            self.log_no_internet(RequestResult(url, None, headers, \"\"))\n            return False\n        if response.status_code != 204:\n            ServerHandler.log_error(\n                \"upload_fail_aws\",\n                RequestResult(\n                    url,\n                    response,\n                    headers,\n                    form.get_all_type(\"text-plain\"),\n                ),\n            )\n\n            self.remove_stored_save_key_data()\n            return False\n        return True\n\n    def print_key(self, key: str, **kwargs: Any):\n        if self.print:\n            color.ColoredText.localize(key, **kwargs)\n\n    def get_codes(self, upload_managed_items: bool = True) -> tuple[str, str] | None:\n        self.save_file.show_ban_message = False\n\n        auth_token = self.get_auth_token()\n        if auth_token is None:\n            return None\n\n        save_key = self.get_save_key()\n\n        if save_key is None:\n            self.remove_stored_save_key_data()\n            return None\n\n        if not self.upload_save_data(save_key):\n            return None\n\n        self.print_key(\"getting_codes\")\n\n        bmd = core.BackupMetaData(self.save_file)\n        meta_data = bmd.create(save_key[\"key\"], upload_managed_items)\n\n        url = f\"{self.save_url}/v2/transfers\"\n        headers = core.AccountHeaders(self.save_file, meta_data).get_headers()\n        headers[\"authorization\"] = \"Bearer \" + auth_token\n\n        response = core.RequestHandler(url, headers, core.Data(meta_data)).post()\n        if response is None:\n            self.log_no_internet(RequestResult(url, None, headers, meta_data))\n            return None\n        json: dict[str, Any] = response.json()\n        status_code = json.get(\"statusCode\", 0)\n        if status_code != 1:\n            ServerHandler.log_error(\n                \"upload_fail_transfers\",\n                RequestResult(url, response, headers, meta_data),\n            )\n            self.remove_stored_auth_token()\n            return None\n        payload = json.get(\"payload\", {})\n        transfer_code = payload.get(\"transferCode\", None)\n        confirmation_code = payload.get(\"pin\", None)\n        if transfer_code is None or confirmation_code is None:\n            ServerHandler.log_error(\n                \"upload_fail_transfers\",\n                RequestResult(url, response, headers, \"\"),\n            )\n            self.remove_stored_auth_token()\n            return None\n        bmd.remove_managed_items()\n        if self.print:\n            print()\n        return (transfer_code, confirmation_code)\n\n    def has_managed_items(self) -> bool:\n        bmd = core.BackupMetaData(self.save_file)\n        managed_items = bmd.get_managed_items()\n        if len(managed_items) == 0:\n            return False\n        return True\n\n    def upload_meta_data(self) -> bool:\n        auth_token = self.get_auth_token()\n        if auth_token is None:\n            return False\n\n        save_key = self.get_save_key()\n        if save_key is None:\n            self.remove_stored_save_key_data()\n            return False\n\n        if not self.upload_save_data(save_key):\n            return False\n\n        bmd = core.BackupMetaData(self.save_file)\n        meta_data = bmd.create(save_key[\"key\"])\n\n        url = f\"{self.save_url}/v2/backups\"\n        headers = core.AccountHeaders(self.save_file, meta_data).get_headers()\n        headers[\"authorization\"] = \"Bearer \" + auth_token\n\n        response = core.RequestHandler(url, headers, core.Data(meta_data)).post()\n        if response is None:\n            self.log_no_internet(RequestResult(url, None, headers, meta_data))\n            return False\n        json: dict[str, Any] = response.json()\n        status_code = json.get(\"statusCode\", 0)\n        if status_code != 1:\n            self.remove_stored_auth_token()\n            return False\n        bmd.remove_managed_items()\n        return True\n\n    def get_new_inquiry_code(self) -> str | None:\n        url = f\"{self.backups_url}/?action=createAccount&referenceId=\"\n\n        response = core.RequestHandler(url).get()\n        if response is None:\n            self.log_no_internet(RequestResult(url, None, {}, \"\"))\n            return None\n        data = response.json()\n        iq = data[\"accountId\"]\n        return iq\n\n    def create_new_account(self) -> bool:\n        new_iq = self.get_new_inquiry_code()\n        if new_iq is None:\n            return False\n        self.save_file.inquiry_code = new_iq\n        self.remove_stored_auth_token()\n        self.remove_stored_save_key_data()\n        self.remove_stored_password()\n        fail_text = \"EXPECT_THIS_TO_FAIL\"\n        start_count = (40 - len(fail_text)) // 2\n        end_count = 40 - len(fail_text) - start_count\n        self.save_file.password_refresh_token = (\n            \"_\" * start_count + fail_text + \"_\" * end_count\n        )\n        password = self.get_password()\n        auth_token = self.get_auth_token()\n        save_key_data = self.get_save_key()\n        self.update_managed_items()\n        self.save_file.show_ban_message = False\n        if password is None or auth_token is None or save_key_data is None:\n            return False\n        return True\n\n    @staticmethod\n    def from_codes(\n        transfer_code: str,\n        confirmation_code: str,\n        cc: core.CountryCode,\n        gv: core.GameVersion,\n        print: bool = True,\n        save_backup: bool = True,\n    ) -> tuple[ServerHandler | None, RequestResult | None]:\n        url = f\"{ServerHandler.save_url}/v2/transfers/{transfer_code}/reception\"\n        data = core.ClientInfo(cc, gv).get_client_info()\n        data[\"pin\"] = confirmation_code\n        data_str = (\n            core.JsonFile.from_object(data)\n            .to_data(indent=None)\n            .to_str()\n            .replace(\" \", \"\")\n        )\n\n        headers = {\n            \"content-type\": \"application/json\",\n            \"accept-encoding\": \"gzip\",\n            \"connection\": \"keep-alive\",\n            \"user-agent\": \"Dalvik/2.1.0 (Linux; U; Android 9; SM-G955F Build/N2G48B)\",\n        }\n        response = core.RequestHandler(url, headers, core.Data(data_str)).post()\n        if response is None:\n            if print:\n                core.print_no_internet()\n            return None, None\n        resp_headers = response.headers\n        content_type = resp_headers.get(\"content-type\", \"\")\n        if content_type != \"application/octet-stream\":\n            return None, RequestResult(url, response, headers, data_str)\n\n        save_data = response.content\n\n        if save_backup:\n            temp_path = core.get_transfer_backup_path()\n            if temp_path is None:\n                temp_path = (\n                    core.Path.get_data_folder()\n                    .add(\"saves\")\n                    .generate_dirs()\n                    .add(\"transfer_backup\")\n                )\n            try:\n                temp_path.write(core.Data(save_data))\n            except Exception as e:\n                color.ColoredText.localize(\n                    \"transfer_backup_fail\", path=str(temp_path), error=e\n                )\n            else:\n                if print:\n                    color.ColoredText.localize(\"transfer_backup\", path=str(temp_path))\n\n        save_file = core.SaveFile(core.Data(save_data), cc=cc)\n\n        password_refresh_token = resp_headers.get(\"Nyanko-Password-Refresh-Token\")\n        if password_refresh_token is not None:\n            save_file.password_refresh_token = password_refresh_token\n\n        server_handler = ServerHandler(save_file)\n        password = resp_headers.get(\"Nyanko-Password\")\n        if password is not None:\n            server_handler.save_password(password)\n\n        return server_handler, RequestResult(url, response, headers, data_str)\n\n    def update_managed_items(self) -> bool:\n        auth_token = self.get_auth_token()\n        if auth_token is None:\n            return False\n        data = {\n            \"catfoodAmount\": self.save_file.catfood,\n            \"isPaid\": True,\n            \"legendTicketAmount\": self.save_file.legend_tickets,\n            \"nonce\": core.Random.get_hex_string(32),\n            \"platinumTicketAmount\": self.save_file.platinum_tickets,\n            \"rareTicketAmount\": self.save_file.rare_tickets,\n        }\n        data_str = (\n            core.JsonFile.from_object(data)\n            .to_data(indent=None)\n            .to_str()\n            .replace(\" \", \"\")\n        )\n        url = f\"{self.managed_item_url}/v1/managed-items\"\n        headers = core.AccountHeaders(self.save_file, data_str).get_headers()\n        headers[\"authorization\"] = \"Bearer \" + auth_token\n        response = core.RequestHandler(url, headers, core.Data(data_str)).post()\n        if response is None:\n            self.log_no_internet(RequestResult(url, None, headers, data_str))\n            return False\n        json: dict[str, Any] = response.json()\n        status_code = json.get(\"statusCode\", 0)\n        if status_code != 1:\n            self.remove_stored_auth_token()\n            return False\n\n        core.BackupMetaData(self.save_file).remove_managed_items()\n        return True\n\n    def download_event_data(self, filename: str) -> core.Data | None:\n        url = (\n            self.events_url\n            + f\"/battlecats{self.save_file.cc.get_patching_code()}_production/{filename}\"\n        )\n\n        auth_token = self.get_auth_token()\n\n        if auth_token is None:\n            return None\n\n        url += f\"?jwt={auth_token}\"\n\n        headers = {\n            \"accept-encoding\": \"gzip\",\n            \"connection\": \"keep-alive\",\n            \"user-agent\": \"Dalvik/2.1.0 (Linux; U; Android 9; Pixel 2 Build/PQ3A.190801.002)\",\n        }\n\n        resp = core.RequestHandler(url, headers).get()\n\n        if resp is None:\n            return None\n\n        return core.Data(resp.content)\n\n    def download_gatya_data(self) -> core.Data | None:\n        return self.download_event_data(\"gatya.tsv\")\n\n    def download_item_data(self) -> core.Data | None:\n        return self.download_event_data(\"item.tsv\")\n\n    def download_sale_data(self) -> core.Data | None:\n        return self.download_event_data(\"sale.tsv\")\n"
  },
  {
    "path": "src/bcsfe/core/server/updater.py",
    "content": "from __future__ import annotations\nimport sys\nfrom typing import Any\nfrom bcsfe import core\nimport bcsfe\n\n\nclass Updater:\n    package_name = \"bcsfe\"\n\n    def __init__(self):\n        pass\n\n    def get_local_version(self) -> str:\n        return bcsfe.__version__\n\n    def get_pypi_json(self) -> dict[str, Any] | None:\n        url = f\"https://pypi.org/pypi/{self.package_name}/json\"\n        # add a User-Agent since pypi started to block the default requests user-agent\n        # this probably won't be needed in the future as i assume this block is temporary\n        response = core.RequestHandler(\n            url, headers={\"User-Agent\": \"BCSFE-Updater\"}\n        ).get()\n        if response is None:\n            return None\n        try:\n            return response.json()\n        except core.JSONDecodeError:\n            return None\n\n    def get_releases(self) -> list[str] | None:\n        pypi_json = self.get_pypi_json()\n        if pypi_json is None:\n            return None\n        releases = pypi_json.get(\"releases\")\n        if releases is None:\n            return None\n        return list(releases.keys())\n\n    def get_latest_version(self, prereleases: bool = False) -> str | None:\n        releases = self.get_releases()\n        if releases is None:\n            return None\n\n        releases.reverse()\n        if prereleases:\n            return releases[0]\n        else:\n            for release in releases:\n                if \"b\" not in release:\n                    return release\n            return releases[0]\n\n    def get_latest_version_info(\n        self, prereleases: bool = False\n    ) -> dict[str, Any] | None:\n        pypi_json = self.get_pypi_json()\n        if pypi_json is None:\n            return None\n        releases = pypi_json.get(\"releases\")\n        if releases is None:\n            return None\n        return releases.get(self.get_latest_version(prereleases))\n\n    def update(self, target_version: str) -> bool:\n        binary = sys.orig_argv[0]\n        python_aliases = [binary, \"py\", \"python\", \"python3\"]\n        for python_alias in python_aliases:\n            cmd = f\"{python_alias} -m pip install --upgrade {self.package_name}=={target_version}\"\n            result = core.Path().run(cmd)\n            if result.exit_code == 0:\n                break\n        else:\n            pip_aliases = [\"pip\", \"pip3\"]\n            for pip_alias in pip_aliases:\n                cmd = f\"{pip_alias} install --upgrade {self.package_name}=={target_version}\"\n                result = core.Path().run(cmd)\n                if result.exit_code == 0:\n                    break\n            else:\n                return False\n        return True\n\n    def has_enabled_pre_release(self) -> bool:\n        return core.core_data.config.get_bool(core.ConfigKey.UPDATE_TO_BETA)\n"
  },
  {
    "path": "src/bcsfe/core/theme_handler.py",
    "content": "from __future__ import annotations\nimport dataclasses\nimport tempfile\nfrom typing import Any\nfrom bcsfe import core\nfrom bcsfe.cli import color\n\n\nclass ThemeHandler:\n    def __init__(self, theme_code: str | None = None):\n        if theme_code is None:\n            self.theme_code = core.core_data.config.get_str(core.ConfigKey.THEME)\n        else:\n            self.theme_code = theme_code\n\n        self.theme_data = self.get_theme_data()\n\n    @staticmethod\n    def get_themes_folder() -> core.Path:\n        return core.Path(\"themes\", True).generate_dirs()\n\n    @staticmethod\n    def get_external_themes_folder() -> core.Path:\n        return core.Path.get_data_folder().add(\"external_themes\").generate_dirs()\n\n    @staticmethod\n    def get_theme_path(theme_code: str) -> core.Path:\n        if theme_code.startswith(\"ext-\"):\n            return ThemeHandler.get_external_themes_folder().add(theme_code + \".json\")\n        return ThemeHandler.get_themes_folder().add(theme_code + \".json\")\n\n    def get_theme_data(self) -> dict[str, Any]:\n        file_path = self.get_theme_path(self.theme_code)\n        if not file_path.exists():\n            return {}\n        try:\n            return core.JsonFile.from_data(file_path.read()).to_object()\n        except core.JSONDecodeError:\n            return {}\n\n    def get_short_name(self) -> str:\n        return self.theme_data.get(\"short_name\", \"\")\n\n    def get_name(self) -> str:\n        return self.theme_data.get(\"name\", \"\")\n\n    def get_description(self) -> str:\n        return self.theme_data.get(\"description\", \"\")\n\n    def get_author(self) -> str:\n        return self.theme_data.get(\"author\", \"\")\n\n    def get_version(self) -> str:\n        return self.theme_data.get(\"version\", \"\")\n\n    def get_git_repo(self) -> str | None:\n        return self.theme_data.get(\"git_repo\", None)\n\n    def get_theme_colors(self) -> dict[str, Any]:\n        return self.theme_data.get(\"colors\", {})\n\n    def get_theme_color(self, color_code: str) -> str:\n        return self.get_theme_colors().get(color_code, \"\")\n\n    def get_primary_color(self) -> str:\n        return self.get_theme_color(\"primary\")\n\n    def get_secondary_color(self) -> str:\n        return self.get_theme_color(\"secondary\")\n\n    def get_tertiary_color(self) -> str:\n        return self.get_theme_color(\"tertiary\")\n\n    def get_quaternary_color(self) -> str:\n        return self.get_theme_color(\"quaternary\")\n\n    def get_error_color(self) -> str:\n        return self.get_theme_color(\"error\")\n\n    def get_warning_color(self) -> str:\n        return self.get_theme_color(\"warning\")\n\n    def get_success_color(self) -> str:\n        return self.get_theme_color(\"success\")\n\n    @staticmethod\n    def get_all_themes() -> list[str]:\n        themes = [\n            file.get_file_name_without_extension()\n            for file in ThemeHandler.get_themes_folder().get_paths_dir(\n                regex=r\".*\\.json\"\n            )\n        ]\n        themes += [\n            folder.get_file_name_without_extension()\n            for folder in ThemeHandler.get_external_themes_folder().get_paths_dir(\n                regex=r\".*\\.json\"\n            )\n        ]\n        return themes\n\n    @staticmethod\n    def remove_theme(theme_code: str):\n        extern = ExternalThemeManager.get_external_theme(theme_code)\n        if extern is not None:\n            ExternalThemeManager.delete_theme(extern)\n\n        ThemeHandler.get_theme_path(theme_code).remove()\n        if theme_code == core.core_data.config.get_str(core.ConfigKey.THEME):\n            core.core_data.config.set_default(core.ConfigKey.THEME)\n\n\n@dataclasses.dataclass\nclass ExternalTheme:\n    short_name: str\n    name: str\n    description: str\n    author: str\n    version: str\n    colors: dict[str, Any]\n    git_repo: str | None = None\n\n    def to_json(self) -> dict[str, Any]:\n        return dataclasses.asdict(self)\n\n    @staticmethod\n    def from_json(json_data: dict[str, Any]) -> ExternalTheme | None:\n        try:\n            return ExternalTheme(**json_data)\n        except TypeError:\n            return None\n\n    @staticmethod\n    def from_git_repo(git_repo: str) -> ExternalTheme | None:\n        repo = core.GitHandler().get_repo(git_repo)\n        if repo is None:\n            return None\n        theme_json = repo.get_file(core.Path(\"theme.json\"))\n        if theme_json is None:\n            return None\n        json_data = core.JsonFile.from_data(theme_json).to_object()\n        json_data[\"git_repo\"] = git_repo\n        return ExternalTheme.from_json(json_data)\n\n    def get_new_version(self) -> bool:\n        if self.git_repo is None:\n            return False\n        repo = core.GitHandler().get_repo(self.git_repo)\n        if repo is None:\n            return False\n        with tempfile.TemporaryDirectory() as tmp:\n            temp_dir = core.Path(tmp)\n            success = repo.clone_to_temp(temp_dir)\n            if not success:\n                return False\n            external_theme = ExternalThemeManager.parse_external_theme(\n                temp_dir.add(\"theme.json\")\n            )\n            if external_theme is None:\n                return False\n            version = external_theme.version\n\n            if version == self.version:\n                return False\n\n            self.name = external_theme.name\n            self.short_name = external_theme.short_name\n            self.description = external_theme.description\n            self.author = external_theme.author\n            self.colors = external_theme.colors\n            self.version = version\n\n        success = repo.pull()\n        if not success:\n            return False\n        self.save()\n        return True\n\n    def save(self):\n        ExternalThemeManager.save_theme(self)\n\n    def get_full_name(self) -> str:\n        return f\"ext-{self.author}-{self.short_name}\"\n\n\nclass ExternalThemeManager:\n    @staticmethod\n    def delete_theme(external_theme: ExternalTheme):\n        if external_theme.git_repo is None:\n            return\n        folder = core.GitHandler.get_repo_folder().add(\n            external_theme.git_repo.split(\"/\")[-1]\n        )\n        folder.remove()\n\n    @staticmethod\n    def save_theme(\n        external_theme: ExternalTheme,\n    ):\n        \"\"\"Saves an external theme.\n\n        Args:\n            external_theme (ExternalTheme): External theme to save.\n        \"\"\"\n        if external_theme.git_repo is None:\n            return\n        file = ThemeHandler.get_theme_path(external_theme.get_full_name())\n\n        json_data = external_theme.to_json()\n        file.write(core.JsonFile.from_object(json_data).to_data())\n\n    @staticmethod\n    def parse_external_theme(path: core.Path) -> ExternalTheme | None:\n        \"\"\"Parses an external theme.\n\n        Args:\n            path (core.Path): Path to the external theme.\n\n        Returns:\n            ExternalTheme: External theme.\n        \"\"\"\n        json_data = core.JsonFile.from_data(path.read()).to_object()\n        return ExternalTheme.from_json(json_data)\n\n    @staticmethod\n    def update_external_theme(external_theme: ExternalTheme):\n        \"\"\"Updates an external theme.\n\n        Args:\n            external_theme (ExternalTheme): External theme to update.\n        \"\"\"\n        if external_theme.git_repo is None:\n            return\n        color.ColoredText.localize(\n            \"checking_for_theme_updates\",\n            theme_name=external_theme.name,\n        )\n        updated = external_theme.get_new_version()\n        if updated:\n            color.ColoredText.localize(\n                \"external_theme_updated\",\n                theme_name=external_theme.name,\n                version=external_theme.version,\n            )\n        else:\n            color.ColoredText.localize(\n                \"external_theme_no_update\",\n                theme_name=external_theme.name,\n                version=external_theme.version,\n            )\n        print()\n\n    @staticmethod\n    def update_all_external_themes(_: Any = None):\n        \"\"\"Updates all external themes.\"\"\"\n        files = ThemeHandler.get_external_themes_folder().get_paths_dir()\n        if not files:\n            color.ColoredText.localize(\n                \"no_external_themes\",\n            )\n            return\n        if not core.GitHandler.is_git_installed():\n            color.ColoredText.localize(\n                \"git_not_installed\",\n            )\n            return\n        for file in files:\n            theme = ExternalThemeManager.parse_external_theme(file)\n            if theme is None:\n                continue\n            ExternalThemeManager.update_external_theme(theme)\n\n    @staticmethod\n    def get_external_theme_config() -> ExternalTheme | None:\n        \"\"\"Gets the external theme from the config.\n\n        Returns:\n            ExternalTheme: External theme.\n        \"\"\"\n\n        theme = core.core_data.config.get_str(core.ConfigKey.THEME)\n        if not theme.startswith(\"ext-\"):\n            return None\n        return ExternalThemeManager.parse_external_theme(\n            ThemeHandler.get_theme_path(theme)\n        )\n\n    @staticmethod\n    def get_external_theme(theme: str) -> ExternalTheme | None:\n        \"\"\"Gets the external theme from the theme code.\n\n        Returns:\n            ExternalTheme: External theme.\n        \"\"\"\n\n        if not theme.startswith(\"ext-\"):\n            return None\n        return ExternalThemeManager.parse_external_theme(\n            ThemeHandler.get_theme_path(theme)\n        )\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/config.properties",
    "content": "config=Config\nedit_config=Edit config\ndefault_value=(default value: <@q>{default_value}</>)\ncurrent_value=(current value: <@q>{current_value}</>)\nconfig_value_txt=<@s>{{current_value}} {{default_value}}</>\n\nconfig_dialog=Select a config option to edit:\n\nupdate_to_beta_desc=Check for updates to beta versions {{config_value_txt}}\nupdate_to_beta=Update to Beta Versions\n\nshow_update_message_desc=Show a message when a new version is available {{config_value_txt}}\nshow_update_message=Show update message\n\nconfig_full=<@t>{key_desc}</>\n\ndisable_maxes_desc=Disable maximum values when editing {{config_value_txt}}\ndisable_maxes=Disable maximum values\n\nmax_backups_desc=Maximum number of backups of save files to keep {{config_value_txt}}\nmax_backups=Maximum save backups\n\navailable_themes=Available themes:\ntheme_desc=Theme to use {{config_value_txt}}\ntheme=Theme\n\nshow_missing_locale_keys=Show missing locale keys\nshow_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}}\n\nreset_cat_data_desc=Reset all cat data when removing a cat from the save file {{config_value_txt}}\nreset_cat_data=Reset cat data on cat removal\n\nfilter_current_cats_desc=When selecting cats to edit, filter out cats that are not in the save file {{config_value_txt}}\nfilter_current_cats=Filter current cats on cat selection \n\nset_cat_current_forms_desc=When true forming cats, set the cat's current form to the newly unlocked form {{config_value_txt}}\nset_cat_current_forms=Set cat current forms on form unlock\n\nstrict_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}}\nstrict_upgrade=Strict upgrade checks\n\nseparate_cat_edit_options_desc=Separate the cat edit options into multiple features {{config_value_txt}}\nseparate_cat_edit_options=Separate cat edit options\n\nstrict_ban_prevention_desc=When doing anything server related, create a new account to reduce the chance of getting banned {{config_value_txt}}\nstrict_ban_prevention=Strict ban prevention\n\nmax_request_timeout_desc=Maximum time to wait for a request to complete (in seconds) {{config_value_txt}}\nmax_request_timeout=Maximum request timeout\n\ngame_data_repo_desc=Repository to use for game data {{config_value_txt}}\ngame_data_repo=Game data repository\ngame_data_repo_dialog=Enter a game data repository to use:\n\nforce_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}}\nforce_lang_game_data=Force use game data for current locale\n\nclear_tutorial_on_load_desc=Clear the tutorial when you load a save file into the editor {{config_value_txt}}\nclear_tutorial_on_load=Clear tutorial on save load\n\nremove_ban_message_on_load_desc=Remove the ban message when you load a save file into the editor {{config_value_txt}}\nremove_ban_message_on_load=Remove ban message on save load\n\nunlock_cat_on_edit_desc=Unlock the cat when you edit its level, talents, form, etc. {{config_value_txt}}\nunlock_cat_on_edit=Unlock cat on edit\n\nuse_file_dialog_desc=Use the tkinter file dialog to open and save files instead of the file input {{config_value_txt}}\nuse_file_dialog=Use file dialog\n\nadb_path_desc=Path to the adb executable {{config_value_txt}}\nadb_path=ADB path\n\nuse_waydroid=Use waydroid shell rather than adb\nuse_waydroid_desc=Waydroid doesn't support adb root, so use waydroid shell instead {{config_value_txt}}\n\nuse_pkexec_waydroid=Use the pkexec binary to run waydroid commands\nuse_pkexec_waydroid_desc=Running <@s>waydroid shell</> requires root access. Use <@s>pkexec</> to avoid running the whole editor as root {{config_value_txt}}\n\nignore_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}}\nignore_parse_error=Ignore Save Parsing Errors\n\nstring_config_dialog=Enter a new value for <@q>{val}</>:\n\n\nenable_disable_dialog=Do you want to <@q>enable</> or <@q>disable</> this feature?:\n\n\nenable=Enable\ndisable=Disable\n\nenabled=Enabled\ndisabled=Disabled\n\nconfig_success=<@su>Successfully updated config</>\n\nyaml_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?\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/files.properties",
    "content": "another_path=Enter path manually\nselect_files_dir=Select files in directory:\nenter_path=Enter file path / location:\nenter_path_dir=Enter folder path / location:\nenter_path_default=Enter file path / location (default: <@t>{default}</>):\ncurrent_files_dir=Current files in directory <@t>{dir}</>:\nother_dir=Enter other directory\nno_files_dir=<@e>No files in directory</>\npath_not_exists=<@e>Path does not exist</>"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/input.properties",
    "content": "input_int=Input a number between <@q>{min}</> and <@q>{max}</>:\nselect_edit=Select options for <@t>{group_name}</>:\ninput_int_default=Input a number between <@q>{min}</> and <@q>{max}</> (default <@q>{default}</>):\ninput_many=Input numbers between <@q>{min}</> and <@q>{max}</> separated by spaces:\ninput_single=Input a number between <@q>{min}</> and <@q>{max}</>:\ninput=Enter a value for <@t>{name}</> (current value: <@q>{value}</>) (max value: <@q>{max}</>):\ninput_min=Enter a value for <@t>{name}</> (current value: <@q>{value}</>) (range: <@q>{min}</> - <@q>{max}</>):\ninput_non_max=Enter a value for <@t>{name}</> (current value: <@q>{value}</>):\ninput_all=Enter a value for all <@t>{name}</> (max value: <@q>{max}</>):\nvalue_changed=<@su>Successfully changed <@s>{name}</> to <@s>{value}</>\nvalue_gave=<@su>Successfully gave the <@s>{name}</>\nall_at_once=Select all options at once\ninvalid_input=<@e>Invalid input. Please try again.</>\ninvalid_input_int=<@e>Invalid input. Please enter a number between <@s>{min}</> and <@s>{max}</></>\nselect_option=Select option:\nfinish=Finish\nfeatures=Features:\ngo_back=Go back\nyes_key=y\nquit_key=q\nrange_input=separated by spaces (e.g <@t>1 2 3 192</>), or enter a range (e.g. <@t>1-43</>) or enter <@t>all</>:\nselect_features=\n>To select a feature, enter\n>- a <@q>number</> corresponding to the number on the left\n>- <@t>text</> to search for a feature\n>You can press <@t>enter</> to view all features\n>Some features are <@t>categories</> and so when selected, will display all of its <@t>sub-features</>\n>Input:\n\nindividual=Individual\nedit_all_at_once=All at once\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/locale.properties",
    "content": "available_locales=Available languages:\nlocale_desc=Language to use {{config_value_txt}}\nlocale=Language\nlocale_dialog=Select a language:\nadd_locale=Add Locale\nremove_locale=Remove Locale\nlocale_remove_dialog=Select locales to remove:\nenter_locale_git_repo=Enter the git repository of the locale (e.g <@t>https://codeberg.org/fieryhenry/ExampleEditorLocale.git</>):\nlocale_already_exists=<@e>A locale with name <@s>{locale_name}</> already exists.</>\\nWould you like to overwrite it? ({{y/n}}):\nlocale_added=<@su>Successfully added localization</>\nchecking_for_locale_updates=Checking for updates to external localization <@t>{locale_name}</>...\nexternal_locale_updated=<@su>Successfully updated external localization <@t>{locale_name}</> to version <@t>{version}<@t>.\\n{{restart_to_see_changes}}</>\nexternal_locale_no_update=<@su>No update needed for external localization <@t>{locale_name}</> latest version is <@t>{version}<@t></></>\ninvalid_git_repo=<@e>Invalid git repository</>\nlocale_cancelled=<@e>Cancelled</>\nrestart_to_see_changes=You will need to restart the editor to see all of the changes\nlocale_changed=<@su>Successfully changed locale to <@t>{locale_name}</>.\\n{{restart_to_see_changes}}</>\nlocale_removed=<@su>Successfully removed locale <@t>{locale_name}</>.\\n{{restart_to_see_changes}}</>\nno_external_locales=<@w>No external locales found</>\n\nmissing_locale_keys=Missing Locale Keys:\nextra_locale_keys=Extra Locale Keys:\n\nlocale_text=\n>Current Locale: <@s>{locale_name}</> (Version: <@s>{locale_version}</>)\n>Made by <@s>{locale_author}</>\n>Locale File Location: <@s>{locale_path}</>\n\ndefault_locale_text_authors=\n>Current Locale: <@s>{name}</>\n>Made by <@s>{authors}</>\n>Locale File Location: <@s>{path}</>\n\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/main.properties",
    "content": "# Full documentation: https://codeberg.org/fieryhenry/ExampleEditorLocale#format-of-the-properties-files\n# color formatting\n#\n# <@p> = primary color\n# <@s> = secondary color\n# <@t> = tertiary color\n# <@q> = quaternary color\n# <@e> = error color\n# <@w> = warning color\n# <@su> = success color\n#\n# </> = close current color\n# When coloring, use the above formatting tags, not the actual color codes / names so that different colors can be used for different themes.\n# You should only use the actual color codes / names if the exact colors are important, such coloring a target red talent orb as red.\n# If you want to write < or > or / in the text, escape them with a backslash (\\) e.g. \\< or \\> or \\/\n#\n# <#rrggbb> = hex color\n#\n# <w> = white\n# <bl> = black\n# <r> = red\n# <g> = green\n# <b> = blue\n# <y> = yellow\n# <m> = magenta\n# <c> = cyan\n# <dy> = dark yellow\n# <dg> = dark grey\n# <db> = dark blue\n# <dc> = dark cyan\n# <dm> = dark magenta\n# <dr> = dark red\n# <dgn> = dark green\n# <lg> = light grey\n# <o> = orange\n\ndownloading=<@su>Downloading <@s>{file_name}</> from <@s>{pack_name}</> with version <@s>{version}</> and country code <@s>{country_code}</>\nfailed_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}</s> Maybe check your internet connection.</>\nfailed_to_get_game_versions=<@e>Failed to get game versions. Maybe check your internet connection.</>\nno_device_error=<@e>No connected devices found</>\nno_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.</>\nexit=Exit\ntkinter_not_found=<@e>tkinter was not found. If you are not on mobile, please install it and try again.</>\ntkinter_not_found_enter_path_file=Please enter the path/location of the {initialfile} file:\ntkinter_not_found_enter_path_file_save=Please enter the path/location to save the {initialfile} file:\ntkinter_not_found_enter_path_dir=Please enter the path/location of the {initialdir} folder instead:\ndiscord_url=https://discord.gg/DvmMgvn5ZB\n\nwelcome=\n><@t>Welcome to the <@s>Battle Cats Save File Editor</>!\n>Made by <@s>fieryhenry</>\n>\n>Codeberg: <@s>https://codeberg.org/fieryhenry/BCSFE-Python</>\n>Discord: <@s>{{discord_url}}</> - Please report any bugs to <@s>#bug-reports</> and suggestions to <@s>#suggestions</>\n>Donate: <@s>https://ko-fi.com/fieryhenry</>\n>\n>Config File Location: <@s>{config_path}</>\n>\n>{theme_text}\n>\n>{locale_text}\n>\n><@q>Thanks To:\n>- <@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/</>\n>- <@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\n>- Anyone who has supported my work for giving me motivation to keep working on this and similar projects: <@s>https://ko-fi.com/fieryhenry</>\n>- Everyone in the discord for giving me saves, reporting bugs, suggesting new features, and for being an amazing community: <@s>{{discord_url}}</></>\n>\n><@w>If you paid for this program, you have been scammed. This program is free and open source.</>\n>\n><@w>Use this tool at your own risk. I am not responsible for any bans or damage caused to your save file.\n>Obviously, the save editor does try to prevent this from happening, but I cannot guarantee that your save is safe.\n>Though if your save does get corrupted please do still report it to the discord.\n>I recommend you to make backups of your save file before editing it.</>\n\nreport_message=Please report this to <@s>#bug-reports</> on the discord: <@s>{{discord_url}}</>\nreport_message_l=please report this to <@s>#bug-reports</> on the discord: <@s>{{discord_url}}</>\ntry_again_message=Please try again. If error persists {{report_message_l}}</>\nall=All\n\nerror=<@e>An error has occurred (<@s>{error}</>, editor version: <@s>{version}</>) {{report_message_l}}\\n{traceback}\nsee_log=<@e>Please see the log file for more details.</>\nmax=max\nnone=None\nunknown=Unknown\n\nleave=\\n<@q>Thank you for using the Battle Cats Save File Editor!</>\nchecking_for_changes=<@t>Checking for changes...</>\nno_changes=<@su>No changes found.</>\nchanges_found=<@su>Changes found.</>\n\ny/n=y/n\nyes=yes\n\ngit_not_installed=<@e>Git is not installed. Please install it, add it to PATH, and try again.</>\nfailed_to_get_repo=<@e>Failed to get repo: \"<@t>{url}</>\". Maybe it doesn't exist, or you have no internet connection</>\nfailed_to_run_git_cmd=<@e>Failed to run git command: \"<@t>{cmd}</>\". Maybe check your internet connection</>\ncancel=Cancel\n\nupdate_external=Update External Content\nupdating_external_content=<@q>Updating external content...</>\n\ndownloading_map_names=<@q>Getting map names... (code: <@t>{code}</>). This may take a while...</>\n\nselect_device=Select device:\n\ncontinue_q=Continue? ({{y/n}}):\n\nno_data_version=<@e>The latest available game data version is not available. This is probably due to internet issues. Please try again.</>\n\nno_feature_with_name=<@e>No feature found with name: <@s>{name}</></>\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/save.properties",
    "content": "save_load_option=Select an option to load the save file\ndownload_save=Download save file using transfer and confirmation code\nselect_save_file=Select save file from file\nadb_pull_save=Pull save file from device using adb\nwaydroid_pull_save=Pull save from waydroid device\nload_save_data_json=Load save data from json\nroot_storage_pull_save=Pull save file from root storage\nsave_save_dialog=Save save file\nsave_downloaded=<@su>Save file downloaded to <@s>{path}</>\nsave_json_dialog=Save save data to json\nload_from_documents=Load save file from <@s>{path}</>\nsave_file_not_found=<@e>Save file not found</>\nsave_file_found=<@su>Loading save from: <@t>{path}</></>\n\nparse_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}}</>\nload_json_fail=<@e>Failed to load save data from json ({error})</>\nparse_json_fail=<@e>Failed to read json file, is your file actually in JSON format?</e>\neditor_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}</></>\nsave_management=Save Management\nsave_save=Save save\nsave_save_file=Save save to specific file\nsave_save_documents=Save save to {path}\nsave_upload=Upload save file to server and get transfer and confirmation code\nunban_account=Unban Account / Fix Save Used Elsewhere Error\n\nadb_push_rerun=Use adb to push the save file to a device (Rerun the game after pushing)\nadb_push=Use adb to push the save file to a device (Do not rerun the game after pushing)\nadb_push_success=<@su>Save file pushed to device</>\nadb_push_fail=<@e>Failed to push save file to device</> ({error})\nadb_rerun_success=<@su>Successfully reran game</>\nadb_rerun_fail=<@e>Failed to rerun game</> ({error})\n\nwaydroid_push_rerun=Push the save file to a waydroid device (Also rerun the game after pushing)\nwaydroid_push=Push the save file to a waydroid device (Do not rerun the game after pushing)\nwaydroid_push_success=<@su>Save file pushed to waydroid device</>\nwaydroid_push_fail=<@e>Failed to push save file to waydroid device</> ({error})\nwaydroid_rerun_success=<@su>Successfully reran game on waydroid device</>\nwaydroid_rerun_fail=<@e>Failed to rerun game on waydroid device</> ({error})\n\nexport_save=Export save file to json\nsave_success=<@su>Save file saved to <@s>{path}</>\nexport_success=<@su>Save data exported to <@s>{path}</>\ninit_save=Reset save file\ninit_save_confirm=Are you sure you want to reset your save file? ({{y/n}}):\ninit_save_success=<@su>Succesfully reset save file</>\n\nadb_pulling=<@q>Pulling save file from device with package name <@s>{package_name}</>with adb ...</>\nadb_pull_fail=<@e>Failed to pull save file from device with package name <@s>{package_name}</> ({error}) with adb\n\nwaydroid_pulling=<@q>Pulling save file from device with package name <@s>{package_name}</> with waydroid ...</>\nwaydroid_pull_fail=<@e>Failed to pull save file from device with package name <@s>{package_name}</> ({error}) with waydroid\n\nstorage_pulling=<@q>Pulling save file from root storage with package name <@s>{package_name}</>...</>\nstorage_pull_fail=<@e>Failed to pull save file from root storage with package name <@s>{package_name}</> ({error})\n\nnot_rooted_error=<@e>Device does not seem to be rooted, or the editor is not running as root</>\n\nupload_items=Upload managed items to server\nupload_items_success=<@su>Successfully uploaded managed items</>\nupload_items_fail=<@e>Failed to upload managed items</>\n\nload_save=Load save file\nload_save_success=<@su>Succesfully loaded save file</>\naccount=Account\n\nsave_before_exit=<@q>Save latest changes before exiting? (<@s>y</>/<@s>n</>):</>\nsave_temp_success=<@su>Succesfully managed to recover save file from temp file</>\nsave_temp_fail=<@e>Failed to recover save file from temp file. Latest save changes are lost</> ({error})\\n{traceback}\nsave_temp_not_found=<@e>Failed to recover save file from temp file. Latest save changes are lost</> (Temp file not found)\n\ncant_detect_cc=<@w>Failed to detect country code from save file. \\nPlease enter your country code manually</>\nfailed_to_load_save_gv=Save file loaded but certain values were not as expected. Error thrown to prevent save corruption\nfailed_to_load_save=Failed to load save file\nfailed_to_save_save=Failed to save save file\n\ngame_version_dialog=Enter game version (e.g <@t>12.2.1</>):\ninvalid_game_version=<@e>Invalid game version</>\ncountry_code_set=<@su>Succesfully set country code to <@s>{cc}</>\ngame_version_set=<@su>Succesfully set game version to <@s>{version}</>\n\nconvert_region=Convert country code (e.g en -\\> jp)\nconvert_version=Convert game version (e.g 12.2.1 -\\> 12.2.0)\n\ncc_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}</>\ngv_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}</>\n\ncreate_new_save_success=<@su>Succesfully created new save file</>\ncreate_new_save=Create new save file\ncreate_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.</>\n\nparse_ignored_error=<@w>WARNING: <@e>{error}<>\\n<@w>Ignoring due to the <@s>Ignore Parse Error</> config flag being set. This may cause issues!</> \n\nselect_package_name=Select package name:\n\nadb_not_installed=\n><@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\n>Current Value: <@s>{path}</>\n>Error: <@s>{error}</></>\n\nwaydroid_not_installed=<@e>Waydroid is not installed, or an error occured: {error}</>\n\nroot_push_not_android_error=<@e>Root push is only available on android devices</>\nroot_push_success=<@su>Successfully wrote save to root storage</>\nroot_push_fail=<@e>Failed to write save to root storage. Error: <@s>{error}</></>\n\nroot_rerun_success=<@su>Successfully reran game</>\nroot_rerun_fail=<@e>Failed to rerun game. Error: <@s>{error}</></>\n\nroot_push=Use root to push save directly to the game\nroot_push_rerun=Use root to push save directly to the game (and rerun the game)\n\nselect_recent=Select recent save:\nrecent_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}</></>\n\nload_recent_saves=Load from recent saves and backups\nno_recent_saves=<@w>No recent saves</>\ncurrent_save=\\nCurrent Save: <@t>{cc}</>-<@t>{gv}</> Inquiry: <@t>{inquiry_code}</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/server.properties",
    "content": "transfer_code=Transfer Code\nenter_transfer_code=Enter Transfer Code:\nconfirmation_code=Confirmation Code\nenter_confirmation_code=Enter Confirmation Code:\ncountry_code=Country Code\ncountry_code_select=Select country code:\ninvalid_codes_error=<@e>Failed to download save file. Please check your transfer code and confirmation code and country code and try again.</>\ndisplay_response_debug_info_q=Do you want to display the response debug info? ({{y/n}}):\nresponse_text_display=\n>URL: <@q>{url}</>\n>Request Headers: <@q>{request_headers}</>\n>Request Body: <@q>{request_body}</>\n>\n>Response Headers: <@q>{response_headers}</>\n>Response Body: <@q>{response_body}</>\n\ndownloading_save_file=Downloading save file from server (transfer code: <@q>{transfer_code}</>, confirmation code: <@q>{confirmation_code}</>, country code: <@q>{country_code}</>)...\nupload_result=\n><@su>\n>Transfer Code: <@s>{transfer_code}</>\n>Confirmation Code: <@s>{confirmation_code}</>\n></>\n\nupload_fail=<@e>Failed to upload save file. {{try_again_message}} {{see_log}}</>\nunban_fail=<@e>Failed to unban account. {{try_again_message}} {{see_log}}</>\nunban_success=<@su>Account unbanned successfully.</>\nupload_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}}):\nstrict_ban_prevention_enabled=<@w>Strict Ban Prevention Enabled. A new account will be created before uploading save file / managed items.</>\ncreate_new_account_success=<@su>Account created successfully.</>\ncreate_new_account_fail=<@e>Failed to create account. {{try_again_message}} {{see_log}}</>\n\nuploading_save_file=<@q>Uploading save file to server...</>\ngetting_codes=<@q>Getting transfer code and confirmation code...</>\ngetting_auth_token=<@q>Getting account auth token...</>\nrefreshing_password=<@q>Refreshing account password...</>\ngetting_password=<@q>Getting account password...</>\ngetting_save_key=<@q>Getting account save key...</>\n\ninquiry_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}}\npassword_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}}\n\nno_internet=<@e>No internet connection. Please check your internet connection and try again.</>\n\ntransfer_backup=<@su>Saved backup transfer save file to <@t>{path}</></>\ntransfer_backup_fail=<@e>Failed to save transfer backup file to <@t>{path}</> due to {error}</>\n\nretry_auth_token=<@e>Failed to get auth token, retrying...</>\n\ndownloading_compressed_data=<@su>Downloading game data from <@s>{url}</></>\nclear_game_data_q=Do you want to clear all downloaded game data? ({{y/n}}):\ncleared_game_data=<@su>Successfully cleared game data</>\n\nvalidating_game_repo=Validating game data repo...\ninvalid_response=<@e>Invalid response code: <@s>{response_code}</>. Expected <@s>200</></>\nno_internet_or_connection_error=<@e>Failed to connect to game data repo</>\ninvalid_url=<@e>Invalid URL</>\n\nuse_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}</t> as an alternative game data repo? ({{y/n}}):\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/theme.properties",
    "content": "theme_text=\n>Current Theme: <@s>{theme_name}</> (Version <@s>{theme_version}</>)\n>Made by <@s>{theme_author}</>\n>Theme File Location: <@s>{theme_path}</>\n\ndefault_theme_text=\n>Current Theme: <@s>Default</>\n>Theme File Location: <@s>{theme_path}</>\n\nchecking_for_theme_updates=Checking for updates to external theme <@t>{theme_name}</>...\nexternal_theme_updated=<@su>Successfully updated external theme <@t>{theme_name}</> to version <@t>{version}<@t>.\\n{{restart_to_see_changes}}</>\nexternal_theme_no_update=<@su>No update needed for external theme <@t>{theme_name}</> latest version is <@t>{version}<@t></>\ntheme_changed=<@su>Successfully changed theme to <@t>{theme_name}</>.\\n{{restart_to_see_changes}}</>\ntheme_removed=<@su>Successfully removed theme <@t>{theme_name}</>.\\n{{restart_to_see_changes}}</>\nno_external_themes=<@w>No external themes found</>\n\n\ntheme_dialog=Select a theme:\nadd_theme=Add theme\nremove_theme=Remove theme\ntheme_remove_dialog=Select themes to remove:\nenter_theme_git_repo=Enter the git repository of the theme (e.g <@t>https://codeberg.org/fieryhenry/ExampleEditorTheme.git</>):\ntheme_already_exists=<@e>A theme with name <@s>{theme_name}</> already exists.</>\\nWould you like to overwrite it? ({{y/n}}):\ntheme_added=<@su>Successfully added theme</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/core/updater.properties",
    "content": "local_version=<@q>Local version: <@s>{local_version}</></>\nlatest_version=<@q>Latest version: <@s>{latest_version}</></>\n\nupdate_check_fail=<@e>Failed to check for updates. Maybe check your internet connection?</>\n\nupdate_available=\n><@q>An update is available: <@s>{latest_version}</>\n>Would you like to update? <@t>({{y/n}})</>:\nupdate_success=\n><@t>Update successful\n>Please restart the application</>\nupdate_fail=\n><@e>Update failed\n>Please update manually</>\n>Command: <@s>pip install --upgrade bcsfe</>\n\nversion_line={{local_version}} | {{latest_version}}\n\ndisable_update_message=Would you like to disable update messages? <@t>({{y/n}}):</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/bannable_items.properties",
    "content": "do_you_want_to_continue=Do you want to continue? ({{y/n}}):\n\ncatfood_warning=<@w>WARNING: Editing in cat food can result in a ban. Use at your own risk.</>\\n{{do_you_want_to_continue}}\nlegend_ticket_warning=<@w>WARNING: Editing in legend tickets can result in a ban. Use at your own risk.</>\\n{{do_you_want_to_continue}}\nrare_ticket_warning=\n><@w>WARNING: Editing in rare tickets can result in a ban. Use at your own risk.</>\n>You can use the rare ticket trade feature to get rare tickets with a lower risk of ban.\nplatinum_ticket_warning=\n><@w>WARNING: Editing in platinum tickets can result in a ban. Use at your own risk.</>\n>You can use the platinum shards feature to get platinum tickets with a lower risk of ban.\n\nselect_an_option_to_continue=Select an option to continue editing {feature_name}:\n\ncontinue_editing=Continue editing {feature_name}\ngo_to_safe_feature=Go to the safer {safer_feature_name} feature\ncancel_editing=Cancel editing {feature_name}\n\nrare_ticket_trade_enter=Enter the number of rare tickets you want to <@q>add</> (max value: <@q>{max}</>) (current amount: <@q>{current}</>):\nrare_ticket_trade_storage_full=<@e>ERROR: You don't have enough space in your cat storage, please free 1 space!</>\nrare_ticket_successfully_traded=\n><@su>Successfully gave {rare_ticket_count} rare tickets.</>\n>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.\n\nrare_tickets_l=rare tickets\nrare_ticket_trade_l=rare ticket trade\n\nrare_ticket_trade_maxed=<@e>ERROR: You already have the maximum amount of rare tickets!\\nPlease use some before running this feature!</>\n\nplatinum_tickets_l=platinum tickets\nplatinum_shards_l=platinum shards"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/cats.properties",
    "content": "total_selected_cats=<@t>{total}</> cats currently selected\nselected_cat=<@t>{name}</> (<@t>{id}</>) is selected\n\ncat=<@t>{name}</> (<@t>{id}</>)\nspecial_skill=<@t>{name}</> (<@t>{id}</>)\nitem=<@t>{name}</> (<@t>{id}</>)\nunrecognised_storage_item=<@e>Unrecognised storage item. Item category: <@s>{item_type}</>. Item id: <@s>{id}</> </>\ncurrent_storage_items=Current storage items:\nstorage_is_empty=Storage is empty\navailable_storage=Available storage space: <@t>{slots}</>\ndisplay_storage=Display storage\nclear_storage=Clear storage\nadd_cats=Add cats\nadd_special_skills=Add special skills / base upgrades\nremove_items=Remove cats / skills\ntoo_many_cats_selected=<@e>Too many cats selected. Maximum is <@s>{max}</>. Got <@s>{current}</></>\ntoo_many_skills_selected=<@e>Too many skills selected. Maximum is <@s>{max}</>. Got <@s>{current}</></>\nneed_x_more_space=<@e>Not enough storage space. Need <@s>{needs}</> more slots</>\nadded_cats=Added cats:\nadded_special_skills=Added special skills:\nselect_special_skills=Select special skills\nremoved_items=Removed items:\ncat_storage=Cat Storage\nstorage_success=<@su>Successfully edited cat storage</>\n\nselect_gv=\n>Enter the game versions to filter by. Examples are:\n>- Get cats in versions <@t>11.5.0</> only: <@t>11.5.0</>,\n>- Get cats in versions <@t>12.4.0</> and <@t>13.0.0</> only <@t>12.4.0 13.0.0</>\n>- Get all cats between versions <@t>12.4.0</> and <@t>13.0.0</> inclusive: <@t>12.4.0-13.0.0</>\n>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</>.\n>Input:\n\npossible_gvs=Possible game versions:\n\nno_valid_gvs_entered=<@w>No valid game versions entered</>\n\nselect_cats_rarity=Select cats based on rarity\nselect_cats_name=Select cats based on name\nselect_cats_obtainable=Select all obtainable cats\nselect_cats_not_obtainable=Select all unobtainable cats\nselect_cats_gatya_banner=Select cats based on gacha banner\nselect_cats_game_version=Select cats by game version\nselect_cats_all=Select all cats\nselect_cats=Select cats:\nand_mode_q=Do you want to filter down the current selection (<@t>1</>), add to it (<@t>2</>) or replace it (<@t>3</>)?:\nselect_rarity=Select cat rarity:\nenter_name=Enter cat name:\nselect_name=Select cat name:\nselect_gatya_banner=Enter gacha banner ids {{range_input}}\ncats=Cats\nedit_cats=Edit cats\nenter_cat_ids=You can find cat IDs here: <@t>https://battlecats.miraheze.org/wiki/Cat_Release_Order</>\\nEnter cat ids {{range_input}}\nselect_cats_id=Select cats by id\nno_cats_found_name=<@w>No cats found with name <@s>{name}</></>\n\nselect_cats_again=Select additional cats\nunlock_cats=Unlock Cats|Get Cats\nremove_cats=Remove Cats\nupgrade_cats=Upgrade Cats\ntrue_form_cats=True Form Cats\nremove_true_form_cats=Remove Cat True Forms\nupgrade_talents_cats=Upgrade Cat Talents\nremove_talents_cats=Remove Cat Talents\nunlock_cat_guide=Claim Cat Guide\nremove_cat_guide=Unclaim Cat Guide\nfinish_edit_cats=Finish editing cats\nselect_edit_cats_option=Select an option to edit cats:\n\nupgrade_success=<@su>Successfully upgraded cats</>\nupgrade_cats_select_mod=Select an option to upgrade cats:\nupgrade_individual=Input an upgrade for each selected cat\nselected_cat_upgrades={{selected_cat}}: <@t>{base_level}<@s>+</>{plus_level}</>\nselected_cat_upgraded=<@t>{name}</> (<@t>{id}</>) has been upgraded to <@t>{base_level}<@s>+</>{plus_level}\nupgrade_all=Input an upgrade to apply to all selected cats\nupgrade_input=\n>Enter an upgrade level. Examples:\n><@t>10<@s>+</>20</> = Base level 10, plus level 20\n><@t>10<@s>+</></> = Base level 10, keep current plus level\n><@t><@s>+</>20</> = Keep current base level, plus level 20\n><@t>10</> = Base level 10, plus level 0\n><@t>5<@q>-</>10<@s>+</>20<@q>-</>30</> = Random base level between 5 and 10, random plus level between 20 and 30\n><@t>5<@q>-</>10<@s>+</></> = Random base level between 5 and 10, keep current plus level\n><@t><@s>+</>20<@q>-</>30</> = Keep current base level, random plus level between 20 and 30\n><@t>{{max}}<@s>+</>{{max}}</> = Max base level, max plus level\n><@t>{{quit_key}}</> = Quit\n>Input:\n\nmax_upgrade=Max Upgrade Level: <@t>{max_base}<@s>+</>{max_plus}</>\n\ninvalid_upgrade_base=<@e>Invalid base level: <@s>{base}</>\ninvalid_upgrade_base_random=<@e>Invalid base level range: <@s>{min}</>-<@s>{max}</>\ninvalid_upgrade_plus=<@e>Invalid plus level: <@s>{plus}</>\ninvalid_upgrade_plus_random=<@e>Invalid plus level range: <@s>{min}</>-<@s>{max}</>\n\nremove_true_form_success=<@su>Successfully removed true forms</>\ntrue_form_success=<@su>Successfully true formed cats</>\n\nremove_success=<@su>Successfully removed cats</>\nunlock_success=<@su>Successfully unlocked cats</>\n\nunlock_cat_guide_success=<@su>Successfully claimed cat guide entries</>\nremove_cat_guide_success=<@su>Successfully unclaimed cat guide entries</>\n\nselect_cats_current=Select currently unlocked cats\nselect_cats_not_unlocked=Select cats that are not unlocked\n\ntalents_version_warning=\n><@w>Warning: The editor's game data does not match this save file's game version. Talents may not work as expected.\n>Save Version: <@s>{save_version}</>\n>Game Data Version: <@s>{data_version}</>\n>If the game data is outdated, it should get updated within the next few days.</>\n\ntalents_success=<@su>Successfully upgraded cat talents</>\ntalents_remove_success=<@su>Successfully removed cat talents</>\ntalents_individual=Edit talents for each selected cat\ntalents_all=Max out talents for all selected cats\nupgrade_talents_select_mod=Select an option to edit cat talents:\nno_talent_data=<@w>There is no talent data for this cat</>\ntalents=Talents\nupgrade_talent_cats=Upgrade Cat Talents\nforce_true_form_cats=Force True Form Cats\nforce_true_form_cats_warning=\n><@w>Warning: Only use this if you know the cat has a true form, otherwise it will lead to a glitched true form.\n>The main use of this option is when the editor's game data is outdated and new true forms have not been added yet.</>\n\nfilter_current_q=Do you want to only select from cats that you have currently unlocked (<@t>1</>) or all cats (<@t>2</>)?:\nselect_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)\nselect_cats_all_option=Select from all cats\n\nunlock_remove_cats=Unlock Cats / Remove Cats\ntrue_form_remove_form_cats=True Form Cats / Remove Cat True Forms\nupgrade_talents_remove_talents_cats=Upgrade Talents / Remove Talents Cats\nunlock_remove_cat_guide=Claim / Unclaim Cat Guide Entries\n\nunlock_remove_q=Do you want to <@t>Unlock</> or <@t>Remove</> cats?:\ntrue_form_remove_form_q=Do you want to <@t>True Form</> cats or <@t>Remove Cat True Forms</>?:\nupgrade_talents_remove_talents_q=Do you want to <@t>Upgrade</> or <@t>Remove</> cat talents?:\nunlock_cat_guide_remove_guide_q=Do you want to <@t>Claim</> or <@t>Unclaim</> cat guide entries?:\n\nfourth_form_remove_form_cats=Ultra Form Cats / Remove Cat Ultra Forms (4th Forms)\nforce_fourth_form_cats=Force Ultra Form Cats (4th Forms)\nfourth_form_success=<@su>Successfully ultra formed cats</>\nremove_fourth_form_success=<@su>Successfully removed ultra forms</>\nfourth_form_cats=Ultra Form Cats\nremove_fourth_form_cats=Remove Cat Ultra Forms\nfourth_form_remove_form_q=Do you want to <@t>Ultra Form</> cats or <@t>Remove Cat Ultra Forms</>?:\nforce_fourth_form_cats_warning=\n><@w>Warning: Only use this if you know the cat has an ultra form, otherwise it will lead to a glitched 4th form.\n>The main use of this option is when the editor's game data is outdated and new forms have not been added yet.</>\n\ngatya_info_progress=Downloading gacha info (<@t>{current}</>/<@t>{total}</>)\nunknown_banner=Unknown banner\nbanner_txt={name} (<@s>{int}</>)\nfilter_down_q_gatya=Do you want to remove duplicate and unknown banners from the list? ({{y/n}}):\n\nselect_cats_non_gatya=Select Non-Gacha Cats\nfinished_cats_selection=Have you finished selecting cats? ({{y/n}}):\n\ndownloading_cat_names=<@su>Downloading cat names from <@s>{url}</></>\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/enemy.properties",
    "content": "total_selected_enemies=<@t>{total}</> enemies currently selected\nunlock_enemy_guide_success=<@su>Successfully unlocked enemy guide entries</>\nremove_enemy_guide_success=<@su>Successfully removed enemy guide entries</>\nselected_enemy=<@t>{name}</> (<@t>{id}</>) is selected\nselect_enemies_valid=Select all enemies in the enemy guide\nselect_enemies_invalid=Select all enemies which are not in the enemy guide\nselect_enemies_all=Select all enemies\nselect_enemies_id=Select enemies by ID\nselect_enemies_name=Select enemies by name\nselect_enemies=Select enemies:\nenter_enemy_ids=You can find enemy IDs here: <@t>https://battlecats.miraheze.org/wiki/Enemy_Release_Order</>\\nEnter enemy IDs {{range_input}}:\nenter_enemy_name=Enter enemy name:\nenemy_not_found_name=<@w>No enemies found with name <@s>{name}</></>\nunlock_enemy_guide=Unlock enemy guide entries\nremove_enemy_guide=Remove enemy guide entries\nenemy_guide=Enemy Guide\nedit_enemy_guide=Enter an option to edit enemy guide entries:\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/fixes.properties",
    "content": "fix_gamatoto_crash=Fix gamatoto from crashing the game\nfix_time_errors=Fix time related issues\n\nfix_ototo_crash=Fix ototo from crashing the game\n\nfix_gamatoto_crash_success=<@su>Sucessfully fixed gamatoto from crashing the game</>\nfix_time_errors_success=<@su>Sucessfully fixed time related issues <@w>(Your device time on both devices must be correct for this to work)</></>\nfix_ototo_crash_success=<@su>Successfully fixed ototo from crashing the game</>\n\nfixes=Fixes\n\nunlock_equip_menu=Unlock Equip Menu\nequip_menu_unlocked=<@su>Successfully unlocked equip menu</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/gambling.properties",
    "content": "reset_wildcat_slots=<@su>Successfully reset wildcat slots</>\nreset_cat_scratcher=<@su>Successfully reset cat scratcher lottery</>\nreset_gambling_events=Reset Wildcat Slots and Cat Scratcher Lottery\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/gamototo.properties",
    "content": "enter_raw_gamatoto_xp=Enter Raw Gamatoto XP\nenter_gamatoto_level=Enter Gamatoto Level\nedit_gamatoto_level_q=Enter an option to edit the gamatoto level:\ngamatoto_xp=Gamatoto XP\ngamatoto_level=Gamatoto Level\ngamatoto_level_success=<@su>Succesfully set gamatoto level to <@s>{level}</> (XP: <@s>{xp}</>)</>\ngamatoto_level_current=<@t>Current gamatoto level is <@q>{level}</> (XP: <@q>{xp}</>)</>\ngamatoto_xp_level=Gamatoto XP / Level\n\ncurrent_gamatoto_helpers=Current Helpers:\ngamatoto_helper=Helper: <@t>{name}</> (rarity: <@t>{rarity_name}</>)\n\nnew_gamatoto_helpers=New Helpers:\ngamatoto_helpers=Gamatoto Helpers\n\nototo_cat_cannon=Ototo Cat Cannon\n\ncurrent_cannon_stats=Current Cannon Stats:\n\ncannon_part=<@t><@q>{name}</>{buffer}(level <@s>{level}</>)</>\ndevelopment={buffer}(Development: <@q>{development}</>)\ncannon_stats={parts}\n\nfoundation=Foundation\nstyle=Style\neffect=Effect\nimproved_foundation=Improved Foundation\nimproved_style=Improved Style\n\nunknown_stage=Unknown Stage (<@s>{stage}</>)\n\nselected_cannon=<@t>Selected cannon: <@q>{name}</></>\nselected_cannon_stage=<@t>Cannon: <@q>{name}</> Current Stage: <@q>{stage}</></>\n\ncannon_edit_type=Do you want to edit each cannon individually or apply edits to all selected cannons at once?:\n\ncannon_dev_level_q=Do you want to edit the development of the cannons or the levels of the cannons?:\ndevelopment_o=Development\nlevel_o=Levels\n\nselect_development=Select development stage:\nselect_cannon=Select Cannon\ncannon_level=Cannon Level\n\ncannon_success=<@su>Succesfully edited ototo cannons</>\n\ncat_shrine=Edit Cat Shrine\nshrine_level=Edit Shrine Level\nshrine_xp=Shrine XP\ncurrent_shrine_xp_level=<@t>Current XP: <@q>{xp}</> (Level: <@q>{level}</>)</>\ncat_shrine_choice_dialog=What do you want to do?:\nshrine_level_dialog=Enter cat shrine level (max: <@q>{max_level}</>):\nshrine_xp_dialog=Enter cat shrine XP (max: <@q>{max_xp}</>):\ncat_shrine_edited=<@su>Succesfully edited cat shrine</>\nmake_catshrine_appear=Show Cat Shrine in Game\nmake_catshrine_disappear=Hide Cat Shrine in Game\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/gatya.properties",
    "content": "event_tickets=Event Tickets / Lucky Tickets\ndownloading_gatya_data=Downloading gacha event data...\ndownload_gatya_data_success=<@su>Successfully downloaded gacha event data</>\ndownload_gatya_data_fail=<@e>Failed to download gacha event data. Maybe try again</>\nsave_gatya_error=<@e>Failed to save gatya data due to {error}</>\ngatya_by_id_q=Do you want to select gacha banners by <@t>ID</> or <@t>name?</>:\nby_id=By ID\nby_name=By Name\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/gold_pass.properties",
    "content": "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):\ngold_pass=Gold Pass / Officer Club\ngold_pass_remove_success=<@su>Succesfully removed the gold pass</>\ngold_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.</>\nofficer_pass_fixed=<@su>Succesfully fixed the officer club from crashing</>\nfix_officer_pass_crash=Fix Officer Club Crashing\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/items.properties",
    "content": "# Note that not all items are here\n\ncatamins=Catamins\ncatfruit=Catfruit\nbase_materials=Base Materials\ninquiry_code=Inquiry Code\nrare_gatya_seed=Rare Gacha Seed\nnormal_gatya_seed=Normal Gacha Seed\nevent_gatya_seed=Event Gacha Seed\nunlocked_slots=Unlocked Slots|Equip Slots|Lineups\npassword_refresh_token=Password Refresh Token\nchallenge_score=Challenge Score\ndojo_score=Dojo Score\nitems=Items\nuser_rank_rewards=Claim User Rank Rewards (Does not give rewards)\n\ncatfood=Cat Food\nxp=XP\nnormal_tickets=Normal Tickets|Basic Tickets|Silver Tickets\nrare_tickets=Rare Tickets|Gold Tickets\nplatinum_tickets=Platinum Tickets\nlegend_tickets=Legend Tickets\n100_million_tickets=100 Million Downloads Tickets|One Hundred Million Downloads Tickets\n100_million_warn=<@w>Note: you will only be able to see and use the tickets if the 100 Million Downloads event is currently active</>\nplatinum_shards=Platinum Shards\nnp=NP\nleadership=Leadership\ncatseyes=Catseyes\nbattle_items=Battle Items\nduration=<@t>{days}</t> days, <@t>{hours}</t> hours, <@t>{minutes}</> minutes, <@t>{seconds}</> seconds\nendless_item_item=<@s>{item}</> : <@s>{int}</>\nendless_items_success=<@su>Successfully edited endless items</>\ninvalid_minute_count=<@e>Invalid minute amount</>\nenter_duration_minutes=Enter the duration in minutes for the endless items to last for (if you enter <@t>infinity</> the items last forever):\ninfinity_duration=<@t>infinity</>\ninfinity=Infinity\nenter_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):\nbattle_items_endless=Endless Battle Items\ntalent_orbs=Talent Orbs\nscheme_items=Scheme Items\nlabyrinth_medals=Labyrinth Medals\nrestart_pack=Restart Pack|Returner Mode\nengineers=Engineers\ngamototo=Gamatoto / Ototo\nspecial_skills=Special Skills / Base Abilities\ntreasure_chests=Treasure Chests\nunknown_treasure_chest_name=Unknown Treasure Chest ({id})\n\nrare_ticket_trade=Rare Ticket Trade\nrare_ticket_trade_feature_name=Rare Ticket Trade (Allows for unbannable rare tickets)\n\nother=Other\ngatya=Gacha\nlevels=Levels / Story / Treasure\ncats_special_skills=Cats / Special Skills\n\ngatya_item_unknown_name=Unknown Item\nunknown_catamin_name=Unknown Catamin <@t>{id}</>\nunknown_catseye_name=Unknown Catseye <@t>{id}</>\nunknown_catfruit_name=Unknown Catfruit <@t>{id}</>\nunknown_labyrinth_medal_name=Unknown Labyrinth Medal <@t>{id}</>\n\nreset_golden_cat_cpus_success=<@su>Successfully reset golden cat CPU uses</>\nreset_golden_cat_cpus=Reset Golden Cat CPU Uses\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/map.properties",
    "content": "tutorial_already_cleared=<@w>You have already cleared the tutorial</>\ntutorial_cleared=<@su>Succesfully cleared tutorial</>\nclear_tutorial=Clear Tutorial\n\nclear_stages=Clear Stages\nunclear_stages=Unclear Stages\nclear_unclear_q=Do you want to <@t>clear</> or <@t>unclear</> stages?:\n\nadd_enigma_stages=Add Enigma Stages\nclear_enigma_stages=Clear Enigma Stages\ncurrent_enigma_stages=Current Enigma Stages:\nenigma_stage=Enigma Stage <@q>{name}</> (id: <@q>{id}</>) \nunknown_enigma_name=Unknown Enigma Name (id: <@q>{id}</>)\nenigma_select=Select Enigma Stages to Add\nenigma_success=<@su>Succesfully added Enigma Stages</>\nwipe_enigma=Do you want to wipe your current enigma stages? ({{y/n}}):\naku_realm_unlocked=<@su>Succesfully unlocked Aku Realm</>\nunlock_aku_realm=Unlock Aku Realm\n\nselect_story_chapters=Select Story Chapters\nchapter_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)\nedit_chapter_progress_all=Enter the progress to set each chapter to {{chapter_progress_txt}}:\nedit_chapter_progress=Enter the progress to set <@t>{chapter_name}</> to {{chapter_progress_txt}}:\nedit_stage_clear_count=Enter the number of times to clear the stage:\nstory_cleared=<@su>Succesfully cleared story</>\nindividual_chapters=Individual Chapters\nall_chapters=All Chapters\nindividual_chapters_dialog=Do you want to edit the clear progress of each chapter <@t>individually</>? or set <@t>all</> chapters to the same progress?:\nindividual_clear_counts=Individual Clear Counts\nall_clear_counts=All Clear Counts\nindividual_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?:\nclear_story=Main Story Chapters|Clear Story\n\nmap_name_star={name} {star} Crown\n\nclear=Clear\nunclear=Unclear\n\noutbreaks=Outbreaks / Zombie Stages\n\nclear_unclear_outbreaks=Do you want to <@t>clear</> or <@t>unclear</> outbreaks?:\nclear_outbreaks_success=<@su>Succesfully cleared outbreaks</>\nunclear_outbreaks_success=<@su>Succesfully uncleared outbreaks</>\nno_valid_outbreaks=<@e>Error: no valid outbreaks found</>\n\naku_chapters=Aku Realm Chapters\naku_clear_success=<@su>Succesfully cleared Aku Realm</>\naku_current_stage=Aku Realm Stage <@q>{name}</> (id: <@q>{id}</>)\n\nitf_timed_scores=Into the Future Timed Scores\nitf_timed_scores_dialog=Do you want to edit timed scores for <@t>whole chapters at once</> or <@t>individual stages</>?\nitf_timed_scores_edited=<@su>Succesfully edited Into The Future timed scores</>\nitf_timed_score_dialog=Enter the timed score:\ncurrent_stage={chapter_name} <@t>{stage_name}</>\nitf_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?:\n\nfilibuster_stage_reclearing_allowed=<@su>Filibuster stage has successfully been re-enabled.</>\nfilibuster_reclearing=Re-enable Filibuster Stage\n\nall_selected_stages=All Selected Stages\n\nunknown_map_name=Unknown Map Name (id: <@q>{id}</>)\nmap_name={name} <@s>(id: <@q>{id}</>)</>\nedit_map_chapters=Select Chapters\n\nclear_whole_chapters=Clear Whole Chapters\nunclear_whole_chapters=Unclear Whole Chapters\n\nclear_specific_stages=Clear Specific Stages\nunclear_specific_stages=Unclear Specific Stages\n\nselect_clear_type=Do you want to <@t>clear whole chapters</> or <@t>clear specific stages</>?:\nselect_unclear_type=Do you want to <@t>unclear whole chapters</> or <@t>unclear specific stages</>?:\n\ncustom_star_count_per_chapter_yn=Do you want to set a custom star/crown count for each chapter? ({{y/n}}):\nmodify_clear_amounts=Setting clear times to <@t>1</> for each selected stage. Do you want to change this? ({{y/n}}):\nclear_amount_chapter=Set a different clear amount for each selected chapter\nclear_amount_all=Set the same clear amount for all selected chapters\nclear_amount_stages=Set a different clear amount for each selected stage\nselect_clear_amount_type=Enter the clear amount setting mode you want to use:\nclear_amount_enter=Enter the clear amount:\ncustom_star_count_per_chapter=Enter star/crown count (max <@q>{max}</>):\n\ncustom_star_count_per_chapter_unclear=\n>Enter the star/crown to remove:\n><@s><@t>1</> = unclear from whole map</>\n><@s><@t>2</> = unclear from 2nd, 3rd and 4th crown/star map</>\n><@s><@t>3</> = unclear from 3rd and 4th crown/star map</>\n><@s><@t>4</> = unclear from 4th crown/star map</>\n>(max <@q>{max}</>):\n\ncurrent_sol_chapter=Chapter <@t>{name}</> (id: <@q>{id}</>)\ncurrent_sol_star=Star/Crown: <@q>{star}</>\ncurrent_sol_stage=Stage <@q>{name}</> (id: <@q>{id}</>)\nmap_chapters_edited=<@su>Succesfully edited chapters</>\nsol=Stories of Legend\nevent=Normal Event Stages\ncollab=Collaboration Event Stages\nselect_map=Select Map\nselect_map_dialog=\n>Select the maps you want to edit\n>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</>)\n>You can also enter the name / part of a name of the map (e.g <@t>{example}</>) to search / select it\n>You can also enter the word <@q>all</> to select all chapters\n>Input:\nno_map_found=<@e>No map found with name <@s>{name}</></>\nfinished_selecting_maps=Have you finished selecting maps? ({{y/n}}):\ncurrent_maps=Current Maps:\n\nselect_stage=Select Stage\n\ngauntlets=Gauntlets\ncollab_gauntlets=Collaboration Gauntlets\nuncanny=Uncanny Legends\ncatamin_stages=Catamin Stages\nbehemoth_culling=Behemoth Culling\nlegend_quest=Legend Quest\ntowers=Towers\nzero_legends=Zero Legends\n\nunclear_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:\n\nselect_stage_progress=Enter the stage to clear up to and including:\n\nzero_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!</>\n\nstages_select=Enter numbers {{range_input}}\n\nchange_clear_amount_catamin=Change Chapter Clear Amount\nclear_unclear_stage_catamin=Clear / Unclear Catamin Stages\ncatamin_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</>?:\n\nselect_map_from_names=Select Map\n\nenter_clear_amount_catamin_map=Enter clear times to set for chapter <@t>{name}</> (ID: <@t>{id}</t>) (<@t>0</> = haven't cleared this chapter, <@t>3</> or more means the chapter disappears):\nenter_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):\n\ncatamin_stage_success=<@su>Successfully edited catamin stages</>\n\ncatamin_clear_amounts_q=Do you want to edit the clear times for each chapter <@t>individually</> or <@t>all at once</>?:\n\ndojo_catclaw_championships=Clear Dojo Catclaw Championships\n\nfinished=Finished\nedit_chapters_q=What do you want to edit?:\n\nclear_whole_chapter=Clear whole chapter\nclear_to_specific_stage=Clear to a specific stage\nclear_whole_q=Do you want to <@t>clear the whole chapter at once</> or <@t>clear up to a specific stage within the chapter</>?:\n\nclear_all=Clear all chapters\nhandle_individually=Edit each chapter individually\n\nclear_chapters_q=Do you want to <@t>clear all selected chapters</> or <@t>edit the progress of each chapter individually</>?:\nmax_stars=Enter the maximum crown (max: <@t>{max}</>):\n\nedit_map_all=Same clear count for all chapters\nedit_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</>?\n\nedit_whole_chapter=Set the same clear count for the whole chapter\nedit_specific_stages=Edit the clear count of specific stages\nedit_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</>?:\n\neach_stage_individually=Set a different clear count for each selected stage\nstage_all_at_once=Set the same clear count for all selected stages\nset_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</>?:\n\nunknown_stage_name=Unknown stage name (index: <@t>{index}</>)\ncurrent_stage_map=<@t>{name} <@s>(index: <@q>{index}</>)</></>\n\nedit_progress_clear=Edit Map Progress\nedit_progress_unclear=Edit Map Progress (Supports unclearing)\nedit_clear_counts=Edit Stage Clear Counts\n\nkeep_selecting=Keep Selecting\nremove_selection=Remove Selection\nfinish_selection=Finish Selection\nmap_selection_q=What do you want to do?\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/medals.properties",
    "content": "medals=Meow Medals\nadd_medals=Add Medals\nremove_medals=Remove Medals\nmedal_add_remove_dialog=Do you want to <@t>add medals</> or <@t>remove medals</>?:\nmedal_string={medal_name}: <@q>{medal_req}</>\nselect_medals=Select medals:\nmedals_added=<@su>Succesfully added meow medals</>\nmedals_removed=<@su>Succesfully removed meow medals</>"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/missions.properties",
    "content": "missions=Catnip Challenges / Missions|Cat Missions\ncomplete_reward=Clear Missions and Don't Claim Rewards\ncomplete_claim=Complete Missions and Claim Rewards\nuncomplete=Uncomplete Mission\nselect_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</>?\nselect_missions=Select missions to edit:\nmissions_edited=<@su>Succesfully edited missions</>"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/playtime.properties",
    "content": "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)/$)\nplaytime_current=Current playtime: {{playtime_str}}\nplaytime_edited=Successfully edited playtime to {{playtime_str}}\nplaytime_hours_prompt=Enter the number of <@t>hours</> to set the playtime to:\nplaytime_minutes_prompt=Enter the number of <@t>minutes</> to set the playtime to:\nplaytime_seconds_prompt=Enter the number of <@t>seconds</> to set the playtime to:\nplaytime=Play Time"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/scheme_items.properties",
    "content": "scheme_items_edit_success=<@su>Succesfully edited scheme items</>\nscheme_items_select_gain=Select scheme items to gain\nscheme_items_select_remove=Select scheme items to remove\ngain_remove_scheme_items=Do you want to <@t>gain</> or <@t>remove</> scheme items?:\ngain_scheme_items=Gain scheme items\nremove_scheme_items=Remove scheme items"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/special_skills.properties",
    "content": "special_skills_dialog=Select a base ability to upgrade\nupgrade_individual_skill=Input an upgrade for each selected skill\nupgrade_all_skills=Input an upgrade to apply to all selected skills\n\nupgrade_skills_select_mod=Select an option to upgrade skills:\n\nselected_skill=<@t>{name}</> is selected\nselected_skill_upgrades={{selected_skill}}: <@t>{base_level}<@s>+</>{plus_level}</>\nselected_skill_upgraded=<@t>{name}</> is upgraded to <@t>{base_level}<@s>+</>{plus_level}\nskills_edited=<@su>Succesfully edited special skills</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/talent_orbs.properties",
    "content": "total_current_orbs=Total Current Orbs: <@q>{total_orbs}</>\ntotal_current_orb_types=Total Current Orb Types: <@q>{total_types}</>\ncurrent_orbs=Current Orbs:\norb_select=Select talent orbs to edit:\nselected_orbs=Selected talent Orbs:\nedit_orbs_individually=Do you want to edit each orb individually (<@q>1</>) or all at once (<@q>2</>)?:\nedit_orbs_all=Input a value to edit all selected orbs to (max <@t>{max}</>):\nfailed_to_load_orbs=Failed to load talent orbs\n\nedit_orbs_help=\n>Help:\n>Available grades: {all_grades_str}\n>Available attributes: {all_attributes_str}\n>Available effects: {all_effects_str}\n><@w>Note: Not all grades and effects will be available for all attributes.</>\n>Example inputs:\n>    <c>aku</> - selects <c>all aku</> orbs\n>    <r>red</> <b>s</> - selects <r>all red</> orbs with <b>s</> grade\n>    <b>alien</> <r>d</> <r>0</> - selects the <b>alien</> orb with <r>d</> grade that increases <r>attack</>.\n>    <o>c</> <dm>1</> - selects the boost stories of legend orb with grade <o>c</>\n>If you want to select <@q>all</> orbs then input:\n>    <@q>*</>\n>If you want to do <@q>multiple selections</> then separate them with a <@q>comma</> like this:\n>    <b>s</> <bl>black</> <g>4</>,<r>d</> <o>3</>,<g>floating</>\n>\n\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/treasures.properties",
    "content": "whole_chapters=Whole Chapters\nindividual_stages=Individual Stages\ntreasure_groups=Treasure Groups / Sets\ntreasure_dialog=Do you want to edit treasures for <@t>whole chapters at once</>, <@t>individual stages</> or individual <@t>treasure groups</>?:\ntreasures_edited=<@su>Succesfully edited treasures</>\nper_chapter=Per Chapter\nall_selected_chapters=All Selected Chapters\nedit_per_chapter=Do you want to edit data for <@t>all selected chapters</> or <@t>each chapter individually</>?:\nno_treasure=No Treasure\ncustom_treasure_level=Custom Treasure Level (<@w>Only edit if you know what you're doing!</>)\ntreasure_level_dialog=Enter the treasure level you want to set:\ncustom_treasure_level_dialog=Enter the custom treasure level you want to set:\nselect_stage_by_id=Select Stages by IDs\nselect_stage_by_name=Select Stages by Names\nselect_stage_dialog=Do you want to select stages by <@t>IDs</> or <@t>Names</>?:\nselect_stage_id=Enter the stage IDs you want to select {{range_input}}\nselect_stages_name=Select stages:\nselect_treasure_groups=Select the treasure groups you want to edit:\nstory_treasures=Story Treasures\ncurrent_chapter=Current Chapter: <@t>{chapter_name}</>\ncurrent_treasure_group=Current Treasure Group: <@t>{treasure_group_name}</>\ngroup_individual=Individual Groups\ngroup_all_at_once=All Selected Groups\nselect_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?:\n"
  },
  {
    "path": "src/bcsfe/files/locales/en/edits/user_rank.properties",
    "content": "claim=Claim\nunclaim=Unclaim\nfix_claimed=Fix Claimed\nclaim_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?:\nselect_ur=Select user rank rewards\nur_claimed_success=<@su>Successfully claimed user rank rewards</>\nur_unclaimed_success=<@su>Successfully unclaimed user rank rewards</>\nur_string=Rank: <@s>{rank}</>: {description}\nur_fix_claimed_success=<@su>Successfully fixed claimed user rank rewards</>"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/config.properties",
    "content": "config=設定\nedit_config=編輯設定\ndefault_value=(預設值: <@q>{default_value}</>)\ncurrent_value=(目前數值: <@q>{current_value}</>)\nconfig_value_txt=<@s>{{current_value}} {{default_value}}</>\n\nconfig_dialog=選擇要編輯的設定項目:\n\nupdate_to_beta_desc=檢查是否有Beta版本更新 {{config_value_txt}}\nupdate_to_beta=更新至Beta版本\n\nshow_update_message_desc=有新版本時顯示訊息 {{config_value_txt}}\nshow_update_message=顯示版本更新\n\nconfig_full=<@t>{key_desc}</>\n\ndisable_maxes_desc=編輯時停用最大值 {{config_value_txt}}\ndisable_maxes=停用最大值\n\nmax_backups_desc=可保留的最大數量存檔備份 {{config_value_txt}}\nmax_backups=存檔備份最大值\n\navailable_themes=可用的主題:\ntheme_desc=要使用的主題 {{config_value_txt}}\ntheme=主題\n\nshow_missing_locale_keys=顯示缺少的翻譯鍵值\nshow_missing_locale_keys_desc=顯示所有存在於英文 (en) ，但當前語系卻缺少的翻譯鍵值。這對除錯很有幫助： {{config_value_txt}}\n\nreset_cat_data_desc=從存檔移除貓咪時重設所有貓咪資料 {{config_value_txt}}\nreset_cat_data=移除貓咪同時重設資料\n\nfilter_current_cats_desc=選擇貓咪編輯時, 過濾掉不在存檔的貓咪 {{config_value_txt}}\nfilter_current_cats=選擇貓咪時過濾掉現有貓咪\n\nset_cat_current_forms_desc=三階貓咪時, 將貓咪設為新解鎖的型態 {{config_value_txt}}\nset_cat_current_forms=解鎖型態時切換到目前型態\n\nstrict_upgrade_desc=升級貓咪時檢查等級排行及遊戲進度使貓咪只能升級到特定等級 {{config_value_txt}}\nstrict_upgrade=嚴格檢查升級\n\nseparate_cat_edit_options_desc=把貓咪編輯選項分為多個獨立功能 {{config_value_txt}}\nseparate_cat_edit_options=拆分貓咪編輯選項\n\nstrict_ban_prevention_desc=執行任何與伺服器相關的操作時建立一個新帳號以降低被鎖帳的機率 {{config_value_txt}}\nstrict_ban_prevention=嚴格防封鎖機制\nmax_request_timeout_desc=等待請求完成的最大時間(秒) {{config_value_txt}}\nmax_request_timeout=最大請求逾時\n\ngame_data_repo_desc=用於遊戲資料的儲存庫 {{config_value_txt}}\ngame_data_repo=遊戲資料儲存庫\ngame_data_repo_dialog=輸入要使用的遊戲資料儲存庫:\n\nforce_lang_game_data_desc=強制編輯器使用目前語系的遊戲資料，即使存檔屬於不同的遊戲版本 {{config_value_txt}}\nforce_lang_game_data=強制使用目前語系的遊戲資料\n\nclear_tutorial_on_load_desc=將存檔載入編輯器時跳過新手教學 {{config_value_txt}}\nclear_tutorial_on_load=存檔時跳過新手教學\n\nremove_ban_message_on_load_desc=把存檔載入編輯器時移除封帳訊息 {{config_value_txt}}\nremove_ban_message_on_load=存檔時移除封帳訊息\n\nunlock_cat_on_edit_desc=當編輯貓咪的等級、本能、型態等數值時自動解鎖該貓咪 {{config_value_txt}}\nunlock_cat_on_edit=編輯貓咪時解鎖\n\nuse_file_dialog_desc=使用 tkinter 檔案對話框來開啟與儲存檔案，而非手動輸入檔案路徑 {{config_value_txt}}\nuse_file_dialog=使用檔案對話框\n\nadb_path_desc=adb執行檔路徑 {{config_value_txt}}\nadb_path=ADB路徑\n\nuse_waydroid=使用 waydroid shell 而非 adb\nuse_waydroid_desc=Waydroid 不支援 adb root，因此改用 waydroid shell {{config_value_txt}}\n\nuse_pkexec_waydroid=使用 pkexec 執行檔來執行 waydroid 指令\nuse_pkexec_waydroid_desc=執行 <@s>waydroid shell</> 需要root權限。 使用 <@s>pkexec</> 以避免用root執行編輯器 {{config_value_txt}}\n\nignore_parse_error_desc=忽略解析錯誤並直接跳過解析剩餘的存檔資料。 <@w>警告：除非你的存檔已損毀才建議這麼做。任何解析問題都應該回報至 Discord 伺服器</> {{config_value_txt}}\nignore_parse_error=忽略存檔解析錯誤\n\nstring_config_dialog=為 <@q>{val}</>輸入新數值:\n\n\nenable_disable_dialog=是否要 <@q>啟用</> 或 <@q>停用</> 這項功能?:\n\n\nenable=啟用\ndisable=停用\n\nenabled=已啟用\ndisabled=已停用\n\nconfig_success=<@su>成功更新設定</>\n\nyaml_create_error=<@e>在 <@s>{path}<@s>創建yaml檔案時失敗,可能是權限不足，請以root/系統管理員身分執行此編輯器\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/files.properties",
    "content": "another_path=手動輸入路徑\nselect_files_dir=選擇目錄中的檔案：\nenter_path=輸入檔案路徑或位置：\nenter_path_dir=輸入資料夾路徑或位置：\nenter_path_default=輸入檔案路徑或位置： (預設： <@t>{default}</>):\ncurrent_files_dir=目前在 <@t>{dir}</>的檔案：\nother_dir=輸入其他目錄\nno_files_dir=<@e>目錄中沒有檔案</>\npath_not_exists=<@e>路徑不存在</>"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/input.properties",
    "content": "input_int=輸入 <@q>{min}</> 到 <@q>{max}</>之間的數值：\nselect_edit=為 <@t>{group_name}</>選擇選項：\ninput_int_default=輸入 <@q>{min}</> 到 <@q>{max}</>之間的數值： (預設值 <@q>{default}</>)：\ninput_many=輸入 <@q>{min}</> 到<@q>{max}</> 之間的數字並以空格分隔：\ninput_single=輸入 <@q>{min}</> 到 <@q>{max}</>之間的數值：\ninput=輸入 <@t>{name}</>的數值 (目前值： <@q>{value}</>) (最大值： <@q>{max}</>)：\ninput_min=輸入 <@t>{name}</>的數值 (目前值： <@q>{value}</>) (範圍： <@q>{min}</> - <@q>{max}</>)：\ninput_non_max=輸入 <@t>{name}</>的數值 (目前值： <@q>{value}</>)：\ninput_all=輸入所有 <@t>{name}</>的數值 (最大值： <@q>{max}</>)：\nvalue_changed=<@su>成功將 <@s>{name}</> 變更為 <@s>{value}</>\nvalue_gave=<@su>成功發送 <@s>{name}</>\nall_at_once=一次全選\ninvalid_input=<@e>輸入無效。請重試。</>\ninvalid_input_int=<@e>輸入無效。請輸入一個介於 <@s>{min}</> 到 <@s>{max}</>的數值</>\nselect_option=選擇選項：\nfinish=完成\nfeatures=功能：\ngo_back=返回\nyes_key=y\nquit_key=q\nrange_input=以空格分隔 (例如<@t>1 2 3 192</>), 或輸入範圍 (例如 <@t>1-43</>) 或輸入 <@t>all</>：\nselect_features=\n>若要選擇功能，輸入\n>- 左側對應的 <@q>number</> 數字\n>- <@t>text</> 來搜尋功能\n>你可以按 <@t>enter</> 來檢視所有功能\n>部分功能為 <@t>分類</>，選取後會顯示該分類下的所有 <@t>子功能</>\n>輸入：\n\nindividual=個別設定\nedit_all_at_once=統一設定\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/locale.properties",
    "content": "available_locales=可用的語言：\nlocale_desc=要使用的語言 {{config_value_txt}}\nlocale=語言\nlocale_dialog=選擇語言：\nadd_locale=新增語系\nremove_locale=移除語系\nlocale_remove_dialog=選擇要移除的語系：\nenter_locale_git_repo=輸入語系的 git 儲存庫 (例如 <@t>https：//codeberg.org/fieryhenry/ExampleEditorLocale.git</>)：\nlocale_already_exists=<@e>名為 <@s>{locale_name}</> 的語系已經存在</>\\n是否要覆蓋 ({{y/n}})：\nlocale_added=<@su>成功新增本地化語系</>\nchecking_for_locale_updates=正在檢查外部語系 <@t>{locale_name}</>的更新...\nexternal_locale_updated=<@su>成功將外部語系 <@t>{locale_name}</> 更新至版本 <@t>{version}<@t>.\\n{{restart_to_see_changes}}</>\nexternal_locale_no_update=<@su>外部語系 <@t>{locale_name}</>無須更新， 最新版本是 <@t>{version}<@t></></>\ninvalid_git_repo=<@e>無效的git儲存庫</>\nlocale_cancelled=<@e>已取消</>\nrestart_to_see_changes=重新啟動編輯器以檢視所有變更\nlocale_changed=<@su>成功將語言變更為 <@t>{locale_name}</>.\\n{{restart_to_see_changes}}</>\nlocale_removed=<@su>成功移除語系 <@t>{locale_name}</>.\\n{{restart_to_see_changes}}</>\nno_external_locales=<@w>找不到外部語系</>\n\nmissing_locale_keys=缺少的語系鍵值：\nextra_locale_keys=多餘的語系鍵值：\n\nlocale_text=\n>目前語言： <@s>{locale_name}</> (版本： <@s>{locale_version}</>)\n>作者：<@s>{locale_author}</>\n>此語系檔案位置： <@s>{locale_path}</>\n\ndefault_locale_text_authors=\n>目前語言： <@s>{name}</>\n>作者： <@s>{authors}</>\n>語系檔案位置： <@s>{path}</>\n\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/main.properties",
    "content": "# Full documentation： https：//codeberg.org/fieryhenry/ExampleEditorLocale#format-of-the-properties-files\n# color formatting\n#\n# <@p> = primary color\n# <@s> = secondary color\n# <@t> = tertiary color\n# <@q> = quaternary color\n# <@e> = error color\n# <@w> = warning color\n# <@su> = success color\n#\n# </> = close current color\n# When coloring, use the above formatting tags, not the actual color codes / names so that different colors can be used for different themes.\n# You should only use the actual color codes / names if the exact colors are important, such coloring a target red talent orb as red.\n# If you want to write < or > or / in the text, escape them with a backslash (\\) e.g. \\< or \\> or \\/\n#\n# <#rrggbb> = hex color\n#\n# <w> = white\n# <bl> = black\n# <r> = red\n# <g> = green\n# <b> = blue\n# <y> = yellow\n# <m> = magenta\n# <c> = cyan\n# <dy> = dark yellow\n# <dg> = dark grey\n# <db> = dark blue\n# <dc> = dark cyan\n# <dm> = dark magenta\n# <dr> = dark red\n# <dgn> = dark green\n# <lg> = light grey\n# <o> = orange\n\ndownloading=<@su>正在從 <@s>{file_name}</> 下載 <@s>{pack_name}</> (版本 <@s>{version}</> ，國家代碼： <@s>{country_code}</>)\nfailed_to_download_game_data=<@e>無法從 <@s>{file_name}</> 下載遊戲資料 <@s>{pack_name}</> (版本： <@s>{version}</> ，國家代碼： <@s>{country_code}</>。網址： <@s>{url}</s> 請檢查你的網路連線。</>\nfailed_to_get_game_versions=<@e>無法取得遊戲版本，檢查你的網路連線</>\nno_device_error=<@e>未找到已連接的裝置</>\nno_package_name_error=<@e>找不到貓咪大戰爭的安裝包。你的裝置可能沒有 root，或者確保至少進入過一次貓咪基地後重試。.</>\nexit=Exit\ntkinter_not_found=<@e>找不到 tkinter。如果你不是在手機上操作，請安裝後重試。</>\ntkinter_not_found_enter_path_file=輸入 {initialfile} 檔案的路徑或位置：\ntkinter_not_found_enter_path_file_save=輸入儲存 {initialfile} 檔案的路徑或位置：\ntkinter_not_found_enter_path_dir=改為輸入 {initialdir} 資料夾的路徑或位置：\ndiscord_url=https：//discord.gg/DvmMgvn5ZB\n\nwelcome=\n><@t>歡迎使用 <@s>貓咪大戰爭存檔編輯器</>!\n>由 <@s>fieryhenry</>製作\n>\n>Codeberg： <@s>https：//codeberg.org/fieryhenry/BCSFE-Python</>\n>Discord： <@s>{{discord_url}}</> - 請到 <@s>#bug-reports</> 回報任何錯誤以及到 <@s>#suggestions</>提供建議\n>贊助： <@s>https：//ko-fi.com/fieryhenry</>\n>\n>設定檔位置： <@s>{config_path}</>\n>\n>{theme_text}\n>\n>{locale_text}\n>\n><@q>感謝：\n>- <@s>Lethal的編輯器</> 給我靈感並幫助我初步如何修改存檔資料以及編輯貓罐頭： <@s>https：//www.reddit.com/r/BattleCatsCheats/comments/djehhn/editoren/</>\n>- <@s>Beeven</> 和 <@s>csehydrogen's</> 的程式碼，幫助我弄清楚如何修改存檔資料： https：//github.com/beeven/battlecats and https：//github.com/csehydrogen/BattleCatsHacker\n>- 任何支持我作品的人，給了我動力繼續開發這個以及類似的專案： <@s>https：//ko-fi.com/fieryhenry</>\n>- Discord 伺服器裡的所有成員，提供存檔、回報錯誤、提供新功能建議，我們組成了一個超棒的社群： <@s>{{discord_url}}</></>\n>\n><@w>如果你有為此程式付費，代表你被騙了。本程式是完全免費且開源的。</>\n>\n><@w>使用此工具需自行承擔風險。我不對任何帳號封鎖或存檔損壞負責。\n>當然，這個存檔編輯器會盡量防止這些情況發生，但我無法百分百保證你的存檔絕對安全。\n>如果你的存檔真的損壞了請到discord頻道回報。\n>強烈建議在編輯之前先備份你的存檔。</>\n\nreport_message=請將此問題回報至 Discord 的 <@s>#bug-reports</> 頻道： <@s>{{discord_url}}</>\nreport_message_l=請將此問題回報至 Discord 的 <@s>#bug-reports</> 頻道： <@s>{{discord_url}}</>\ntry_again_message=請再試一次。如果錯誤持續發生， {{report_message_l}}</>\nall=全部\n\nerror=<@e>發生錯誤 (<@s>{error}</>, 編輯器版本： <@s>{version}</>) {{report_message_l}}\\n{traceback}\nsee_log=<@e>請查閱紀錄檔獲取更多詳細資訊</>\nmax=最大值\nnone=無\nunknown=未知的\n\nleave=\\n<@q>感謝你使用貓咪大戰爭存檔編輯器！</>\nchecking_for_changes=<@t>正在檢查變更...</>\nno_changes=<@su>未發現任何變更。</>\nchanges_found=<@su>已發現變更。</>\n\ny/n=y/n\nyes=yes\n\ngit_not_installed=<@e>尚未安裝 Git。請先安裝並將其加入系統 PATH路徑之後重試一次 。</>\nfailed_to_get_repo=<@e>無法取得儲存庫： \"<@t>{url}</>\"。 網址可能不存在或你的網路連線中斷</>\nfailed_to_run_git_cmd=<@e>無法執行 git 指令： \"<@t>{cmd}</>\"。 檢查你的網路連線</>\ncancel=取消\n\nupdate_external=更新外部內容\nupdating_external_content=<@q>正在更新外部內容...</>\n\ndownloading_map_names=<@q>正在取得地圖名稱.... (代碼： <@t>{code}</>)。 這可能需要一點時間...</>\n\nselect_device=選擇裝置：\n\ncontinue_q=是否繼續？ ({{y/n}})：\n\nno_data_version=<@e>無法取得最新可用的遊戲版本資料。可能是網路問題，請再試一次。</>\n\nno_feature_with_name=<@e>找不到名為 <@s>{name}</>的功能</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/save.properties",
    "content": "save_load_option=選擇載入存檔的方式\ndownload_save=使用轉移碼和認證碼下載存檔\nselect_save_file=從檔案中選擇存檔\nadb_pull_save=使用 ADB 從裝置提取存檔\nwaydroid_pull_save=從 Waydroid 裝置提取存檔\nload_save_data_json=從 JSON 載入存檔資料\nroot_storage_pull_save=從根目錄提取存檔\nsave_save_dialog=儲存存檔\nsave_downloaded=<@su>存檔已下載至 <@s>{path}</>\nsave_json_dialog=將存檔資料儲存為 JSON\nload_from_documents=從文件資料夾載入存檔\nsave_file_not_found=<@e>找不到存檔</>\nsave_file_found=<@su>正在從 <@t>{path}<@t>中載入存檔</>\n\nparse_save_error=<@e>解析存檔時發生錯誤： {error}\\n(編輯器版本： <@s>{version}</>) (遊戲版本： <@s>{game_version}</>) (國家代碼： <@s>{country_code}</>)\\n{{report_message}}</>\nload_json_fail=<@e>無法從 JSON 載入存檔資料 ({error})</>\nparse_json_fail=<@e>無法讀取 JSON 檔案，請確認檔案是否真的是 JSON 格式</e>\neditor_version_mismatch=<@w>輯器版本不符。存檔可能與此編輯器不相容。JSON 版本： <@t>{json_version}</>, 編輯器版本： <@t>{editor_version}</></>\nsave_management=管理存檔\nsave_save=儲存存檔\nsave_save_file=將存檔另存為特定檔案\nsave_save_documents=將存檔儲存至文件資料夾\nsave_upload=將存檔上傳至伺服器並獲取轉移碼和認證碼\nunban_account=解鎖帳號 / 修復「存檔已在其他裝置使用」錯誤\n\nadb_push_rerun=使用 ADB 將存檔推送至裝置（推送後重啟遊戲）\nadb_push=使用 ADB 將存檔推送至裝置（推送後不重啟遊戲）\nadb_push_success=<@su>已將存檔推送至裝置</>\nadb_push_fail=<@e>無法將存檔推送至裝置</> ({error})\nadb_rerun_success=<@su>已成功重新啟動遊戲</>\nadb_rerun_fail=<@e>無法重新啟動遊戲</> ({error})\n\nwaydroid_push_rerun=將存檔推送至 Waydroid 裝置（推送後重啟遊戲）\nwaydroid_push=將存檔推送至 Waydroid 裝置（推送後不重啟遊戲）\nwaydroid_push_success=<@su>已將存檔推送至 Waydroid 裝置</>\nwaydroid_push_fail=<@e>>無法將存檔推送至 Waydroid 裝置</> ({error})\nwaydroid_rerun_success=<@su>已成功在 Waydroid 裝置上重新啟動遊戲</>\nwaydroid_rerun_fail=<@e>無法在 Waydroid 裝置上重新啟動遊戲</> ({error})\n\nexport_save=將存檔匯出為 JSON\nsave_success=<@su>存檔已儲存至 <@s>{path}</>\nexport_success=<@su>存檔資料已匯出至 <@s>{path}</>\ninit_save=重設存檔\ninit_save_confirm=你確定要重設你的存檔嗎？ ({{y/n}})：\ninit_save_success=<@su>S已成功重設存檔</>\n\nadb_pulling=<@q>正在使用 ADB 從名稱為 <@s>{package_name}</>的裝置提取存檔...</>\nadb_pull_fail=<@e>使用 ADB 從名稱為 <@s>{package_name}</> 的裝置提取存檔失敗({error}) \n\nwaydroid_pulling=<@q>正在使用 Waydroid 從名稱為<@s>{package_name}</> 的裝置提取存檔...</>\nwaydroid_pull_fail=<@e>使用 Waydroid 從名稱為 <@s>{package_name}</>的裝置提取存檔失敗 ({error}) \n\nstorage_pulling=<@q>正在從根目錄提取名稱為 <@s>{package_name}</>的存檔...</>\nstorage_pull_fail=<@e>從根目錄提取名稱為 <@s>{package_name}</>的存檔失敗 ({error})\n\nnot_rooted_error=<@e>裝置似乎沒有 root，或者編輯器未以 root 權限執行</>\n\nupload_items=上傳管理項目到伺服器\nupload_items_success=<@su>已成功上傳管理項目</>\nupload_items_fail=<@e>上傳管理項目時失敗</>\n\nload_save=載入存檔\nload_save_success=<@su>成功載入存檔</>\naccount=帳號\n\nsave_before_exit=<@q>離開前是否儲存最新的變更？ (<@s>y</>/<@s>n</>)：</>\nsave_temp_success=<@su>已成功從暫存檔中復原存檔</>\nsave_temp_fail=<@e>無法從暫存檔中復原存檔。最新的存檔變更已遺失</> ({error})\\n{traceback}\nsave_temp_not_found=<@e>無法從暫存檔中復原存檔。最新的存檔變更已遺失</> (Temp file not found)\n\ncant_detect_cc=<@w>無法從存檔中偵測到國家代碼。\\n請手動輸入你的國家代碼</>\nfailed_to_load_save_gv=存檔已載入，但某些數值不符預期。 已停止操作防止存檔毀損\nfailed_to_load_save=無法載入存檔\nfailed_to_save_save=無法儲存存檔\n\ngame_version_dialog=輸入遊戲版本 (例如 <@t>12.2.1</>)：\ninvalid_game_version=<@e>遊戲版本無效</>\ncountry_code_set=<@su>已成功將國家代碼設定為 <@s>{cc}</>\ngame_version_set=<@su>已成功將遊戲版本設定為 <@s>{version}</>\n\nconvert_region=轉換國家代碼 (例如 en -\\> jp)\nconvert_version=轉換遊戲版本 (例如 12.2.1 -\\> 12.2.0)\n\ncc_warning=<@w>警告：這可能會對你的存檔出現問題、錯誤和當機情況！若要回報錯誤，請確保提及你曾使用過此功能。執行此功能後，你必須進入貓咪基地/升級選單，以便遊戲對存檔進行必要變更。</>\\n目前國家代碼： <@t>{current}</>\ngv_warning=<@w>警告：這可能會對你的存檔出現問題、錯誤和當機情況！若要回報錯誤，請確保提及你曾使用過此功能。執行此功能後，你必須進入貓咪基地/升級選單，以便遊戲對存檔進行必要變更。</>\\n目前遊戲版本： <@t>{current}</>\n\ncreate_new_save_success=<@su>已成功建立新存檔</>\ncreate_new_save=建立新存檔\ncreate_new_save_warning=<@w>警告：許多編輯器功能無法在使用自動建立的存檔時運作，你需要先在遊戲中載入存檔，然後再於編輯器中重新載入\\n此情況可能會在後續的版本中改進。</>\n\nparse_ignored_error=<@w>警告： <@e>{error}<>\\n<@w>由於已設定 <@s>忽略存檔解析錯誤</> 因此將予以忽略。這可能會導致問題！</> \n\nselect_package_name=選擇套件名稱：\n\nadb_not_installed=\n><@e>adb 未加入 PATH 環境變數，或執行檔路徑不正確。請嘗試在設定中編輯 adb 路徑\n>目前數值： <@s>{path}</>\n>錯誤： <@s>{error}</></>\n\nwaydroid_not_installed=<@e>尚未安裝 Waydroid或發生錯誤： {error}</>\n\nroot_push_not_android_error=<@e>Root 推送功能僅適用於 Android 裝置</>\nroot_push_success=<@su>已成功將存檔寫入根目錄</>\nroot_push_fail=<@e>無法將存檔寫入根目錄。錯誤： <@s>{error}</></>\n\nroot_rerun_success=<@su>已成功重新啟動遊戲</>\nroot_rerun_fail=<@e>無法重新啟動遊戲。錯誤： <@s>{error}</></>\n\nroot_push=使用 root 將存檔直接推送至遊戲\nroot_push_rerun=使用 root 將存檔直接推送至遊戲 (並重新啟動遊戲)\n\nselect_recent=選擇最近的存檔：\nrecent_save=<@s><@q>{inquiry_code}</> <@t>{cc}</>-<@t>{gv}</> @ <@t>{year}</>-<@t>{month}</>-<@t>{day}</> <@t>{hour}</>：<@t>{minute}</>：<@t>{second}</> - 原始路徑： <@q>{name}</></>\n\nload_recent_saves=從最近的存檔與備份中載入\nno_recent_saves=<@w>沒有最近的存檔</>\ncurrent_save=\\n目前存檔： <@t>{cc}</>-<@t>{gv}</> 詢問碼： <@t>{inquiry_code}</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/server.properties",
    "content": "transfer_code=轉移碼\nenter_transfer_code=輸入轉移碼：\nconfirmation_code=認證碼\nenter_confirmation_code=輸入認證碼：\ncountry_code=國家代碼\ncountry_code_select=選擇國家代碼：\ninvalid_codes_error=<@e>無法下載存檔。請檢查你的轉移碼、認證碼和國家代碼之後重試</>\ndisplay_response_debug_info_q=是否要顯示回應除錯資訊？({{y/n}})：\nresponse_text_display=\n>網址： <@q>{url}</>\n>請求標頭： <@q>{request_headers}</>\n>請求主體： <@q>{request_body}</>\n>\n>回應標頭： <@q>{response_headers}</>\n>回應主體： <@q>{response_body}</>\n\ndownloading_save_file=正在從伺服器下載存檔（轉移碼： <@q>{transfer_code}</>, 認證碼： <@q>{confirmation_code}</>, 國家代碼： <@q>{country_code}</>)...\nupload_result=\n><@su>\n>轉移碼： <@s>{transfer_code}</>\n>認證碼： <@s>{confirmation_code}</>\n></>\n\nupload_fail=<@e>無法上傳存檔。 {{try_again_message}} {{see_log}}</>\nunban_fail=<@e>無法解除帳號封鎖狀態。 {{try_again_message}} {{see_log}}</>\nunban_success=<@su>已成功解除帳號封鎖。</>\nupload_items_checker_confirm=目前的存檔中有部分管理項目尚未被追蹤。你要現在上傳它們嗎？ ({{y/n}})：\nstrict_ban_prevention_enabled=<@w>已啟用嚴格防封鎖機制。在上傳存檔 / 管理項目之前會建立一個新帳號。</>\ncreate_new_account_success=<@su>已成功建立帳號。</>\ncreate_new_account_fail=<@e>無法建立帳號。 {{try_again_message}} {{see_log}}</>\n\nuploading_save_file=<@q>正在將存檔上傳至伺服器...</>\ngetting_codes=<@q>正在獲取轉移碼與認證碼...</>\ngetting_auth_token=<@q>正在獲取帳號驗證權杖 (Auth Token)...</>\nrefreshing_password=<@q>正在重新整理帳號密碼...</>\ngetting_password=<@q>正在獲取帳號密碼...</>\ngetting_save_key=<@q>正在獲取帳號存檔金鑰 (Save Key)...</>\n\ninquiry_code_warning=<@w>警告：改變詢問碼可能會導致帳號無法遊玩。請自行承擔風險。</>\\n{{do_you_want_to_continue}}\npassword_refresh_token_warning=<@w>警告：編輯密碼重整權杖可能會導致你的帳號無法遊玩。請自行承擔風險。</>\\n{{do_you_want_to_continue}}\n\nno_internet=<@e沒有網路連線。請檢查網路狀態後再試一次。</>\n\ntransfer_backup=<@su>已將備份轉移存檔至 <@t>{path}</></>\ntransfer_backup_fail=<@e>因 {error}，無法將備份轉移存檔儲存至 <@t>{path}</></>\n\nretry_auth_token=<@e>無法獲取驗證權杖，正在重試...</>\n\ndownloading_compressed_data=<@su>正在從 <@s>{url}</>下載遊戲資料</>\nclear_game_data_q=你要清除所有已下載的遊戲資料嗎？({{y/n}})：\ncleared_game_data=<@su>已成功清除遊戲資料</>\n\nvalidating_game_repo=正在驗證遊戲資料儲存庫...\ninvalid_response=<@e>I無效的回應代碼： <@s>{response_code}</>。應為 <@s>200</></>\nno_internet_or_connection_error=<@e>無法連線至遊戲資料儲存庫</>\ninvalid_url=<@e>無效的網址</>\n\nuse_alternative_repo=<@e>無法取得遊戲資料儲存庫，可能因為網路連線有問題，或者該儲存庫在你的網路環境被封鎖。</> 是否要切換到 <@t>{repo}</t> 作為替代的遊戲資料儲存庫？ ({{y/n}})：\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/theme.properties",
    "content": "theme_text=\n>目前主題： <@s>{theme_name}</> (版本 <@s>{theme_version}</>)\n>作者 <@s>{theme_author}</>\n>主題檔案位置： <@s>{theme_path}</>\n\ndefault_theme_text=\n>目前主題： <@s>Default</>\n>主題檔案位置： <@s>{theme_path}</>\n\nchecking_for_theme_updates=正在檢查外部主題 <@t>{theme_name}</> 的更新...\nexternal_theme_updated=<@su>已成功將外部主題 <@t>{theme_name}</> 更新至版本 <@t>{version}<@t>。\\n{{restart_to_see_changes}}</>\nexternal_theme_no_update=<@su>外部主題 <@t>{theme_name}</> 無須更新，最新版本已是 <@t>{version}<@t></>\ntheme_changed=<@su>已成功將主題變更為 <@t>{theme_name}</>。\\n{{restart_to_see_changes}}</>\ntheme_removed=<@su>已成功移除主題 <@t>{theme_name}</>。\\n{{restart_to_see_changes}}</>\nno_external_themes=<@w>找不到外部主題</>\n\n\ntheme_dialog=選擇一個主題：\nadd_theme=新增主題\nremove_theme=移除主題\ntheme_remove_dialog=選擇要移除的主題：\nenter_theme_git_repo=輸入主題的 git 儲存庫 (例如 <@t>https://codeberg.org/fieryhenry/ExampleEditorTheme.git</>)：\ntheme_already_exists=<@e>名為 <@s>{theme_name}</> 的主題已經存在。</>\\n要覆蓋它嗎？({{y/n}})：\ntheme_added=<@su>成功新增主題</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/core/updater.properties",
    "content": "local_version=<@q>本機版本：<@s>{local_version}</></>\nlatest_version=<@q>最新版本：<@s>{latest_version}</></>\n\nupdate_check_fail=<@e>檢查更新失敗。檢查網路連線</>\n\nupdate_available=\n><@q>有可用的更新：<@s>{latest_version}</>\n>是否要更新？<@t>({{y/n}})</>：\nupdate_success=\n><@t>更新成功\n>請重新啟動程式</>\nupdate_fail=\n><@e>更新失敗\n>請手動更新</>\n>指令：<@s>pip install --upgrade bcsfe</>\n\nversion_line={{local_version}} | {{latest_version}}\n\ndisable_update_message=是否停用更新提示訊息？<@t>({{y/n}})：</>"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/bannable_items.properties",
    "content": "do_you_want_to_continue=是否要繼續？ ({{y/n}})：\n\ncatfood_warning=<@w>警告：修改貓罐頭可能會導致被鎖帳。請自行承擔風險。</>\\n{{do_you_want_to_continue}}\nlegend_ticket_warning=<@w>警告：修改傳說券可能會導致被鎖帳。請自行承擔風險。</>\\n{{do_you_want_to_continue}}\nrare_ticket_warning=\n><@w>警告：修改稀有券可能會導致被封鎖。請自行承擔風險。</>\n>你可以使用「稀有券兌換」功能來獲取稀有券，以降低被封鎖的風險。\nplatinum_ticket_warning=\n><@w>警告：修改白金券可能會導致被封鎖。請自行承擔風險。</>\n>你可以使用「白金券碎片」功能來獲取白金券，以降低被封鎖的風險。\n\nselect_an_option_to_continue=選擇一個選項以繼續編輯 {feature_name}：\n\ncontinue_editing=繼續編輯 {feature_name}\ngo_to_safe_feature=前往更安全的 {safer_feature_name} 功能\ncancel_editing=取消編輯 {feature_name}\n\nrare_ticket_trade_enter=輸入你想要<@q>增加</>的稀有券數量（最大值：<@q>{max}</>）（目前數量：<@q>{current}</>）：\nrare_ticket_trade_storage_full=<@e>錯誤：貓咪儲藏庫空間不足，請空出 1 個位子！</>\nrare_ticket_successfully_traded=\n><@su>已成功給予 {rare_ticket_count} 張稀有券。</>\n>你現在需要進入貓咪儲藏庫，按下<@q>全部使用</>按鈕，然後按下<@q>收集換券</>按鈕來獲取稀有券。\n\nrare_tickets_l=稀有券\nrare_ticket_trade_l=稀有券兌換\n\nrare_ticket_trade_maxed=<@e>錯誤：你已經擁有數量上限的稀有券！\\n請在使用此功能前先消耗一些！</>\n\nplatinum_tickets_l=白金券\nplatinum_shards_l=白金券碎片"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/cats.properties",
    "content": "total_selected_cats=目前已選取 <@t>{total}</> 隻貓咪\nselected_cat=已選取 <@t>{name}</> (<@t>{id}</>)\n\ncat=<@t>{name}</> (<@t>{id}</>)\nspecial_skill=<@t>{name}</> (<@t>{id}</>)\nitem=<@t>{name}</> (<@t>{id}</>)\nunrecognised_storage_item=<@e>無法識別的儲藏庫物品。類別：<@s>{item_type}</>。物品 ID：<@s>{id}</> </>\ncurrent_storage_items=目前的儲藏庫物品：\nstorage_is_empty=儲藏庫是空的\navailable_storage=可用儲藏庫空間：<@t>{slots}</>\ndisplay_storage=顯示儲藏庫\nclear_storage=清空儲藏庫\nadd_cats=新增貓咪\nadd_special_skills=新增特殊能力 / 貓炮升級\nremove_items=移除貓咪 / 能力\ntoo_many_cats_selected=<@e>選取的貓咪過多。最大值為 <@s>{max}</>。目前已選取 <@s>{current}</></>\ntoo_many_skills_selected=<@e>選取的能力過多。最大值為 <@s>{max}</>。目前已選取 <@s>{current}</></>\nneed_x_more_space=<@e>儲藏庫空間不足。還需要 <@s>{needs}</> 個空位</>\nadded_cats=已新增貓咪：\nadded_special_skills=已新增特殊能力：\nselect_special_skills=選擇特殊能力\nremoved_items=已移除物品：\ncat_storage=貓咪儲藏庫\nstorage_success=<@su>已成功編輯貓咪儲藏庫</>\n\nselect_gv=\n>輸入遊戲版本來進行篩選。例如：\n>- 僅獲取版本 <@t>11.5.0</> 的貓咪：<@t>11.5.0</>\n>- 僅獲取版本 <@t>12.4.0</> 與 <@t>13.0.0</> 的貓咪：<@t>12.4.0 13.0.0</>\n>- 獲取版本 <@t>12.4.0</> 與 <@t>13.0.0</> 之間（含）的所有貓咪：<@t>12.4.0-13.0.0</>\n>請注意，任何沒有出現在升級選單裡的貓咪都無法在此選擇，因為它們的遊戲版本被設定為 <@t>-1</>。\n>輸入：\n\npossible_gvs=可能的遊戲版本：\n\nno_valid_gvs_entered=<@w>未輸入有效的遊戲版本</>\n\nselect_cats_rarity=依稀有度選擇貓咪\nselect_cats_name=依名稱選擇貓咪\nselect_cats_obtainable=選擇所有可獲得的貓咪\nselect_cats_not_obtainable=選擇所有不可獲得的貓咪\nselect_cats_gatya_banner=依轉蛋系列選擇貓咪\nselect_cats_game_version=依遊戲版本選擇貓咪\nselect_cats_all=選擇所有貓咪\nselect_cats=選擇貓咪：\nand_mode_q=你想要縮減目前的選取範圍 (<@t>1</>)、加入到選取範圍 (<@t>2</>)，還是取代它 (<@t>3</>)？：\nselect_rarity=選擇貓咪稀有度：\nenter_name=輸入貓咪名稱：\nselect_name=選擇貓咪名稱：\nselect_gatya_banner=輸入轉蛋系列 ID {{range_input}}\ncats=貓咪\nedit_cats=編輯貓咪\nenter_cat_ids=你可以在這裡找到貓咪 ID：<@t>https://battlecats.miraheze.org/wiki/Cat_Release_Order</>\\n輸入貓咪 ID {{range_input}}\nselect_cats_id=依 ID 選擇貓咪\nno_cats_found_name=<@w>找不到名稱為 <@s>{name}</> 的貓咪</>\n\nselect_cats_again=選擇額外貓咪\nunlock_cats=解鎖貓咪|獲取貓咪\nremove_cats=移除貓咪\nupgrade_cats=升級貓咪\ntrue_form_cats=三階進化 (第三型態/覺醒)\nremove_true_form_cats=移除貓咪三階\nupgrade_talents_cats=升級貓咪本能\nremove_talents_cats=移除貓咪本能\nunlock_cat_guide=解鎖貓咪圖鑑\nremove_cat_guide=取消解鎖貓咪圖鑑\nfinish_edit_cats=完成貓咪編輯\nselect_edit_cats_option=選擇一個選項來編輯貓咪：\n\nupgrade_success=<@su>已成功升級貓咪</>\nupgrade_cats_select_mod=選擇升級貓咪的選項：\nupgrade_individual=為每隻選取的貓咪個別輸入等級\nselected_cat_upgrades={{selected_cat}}：<@t>{base_level}<@s>+</>{plus_level}</>\nselected_cat_upgraded=<@t>{name}</> (<@t>{id}</>) 已升級至 <@t>{base_level}<@s>+</>{plus_level}\nupgrade_all=輸入等級並套用至所有選取的貓咪\nupgrade_input=\n>輸入等級，範例如下：\n><@t>10<@s>+</>20</> = 等級 10, 加值 20\n><@t>10<@s>+</></> =等級 10, 維持目前加值\n><@t><@s>+</>20</> = 維持目前等級, 加值 20\n><@t>10</> = 等級 10, 加值 0\n><@t>5<@q>-</>10<@s>+</>20<@q>-</>30</> =  5 到 10 之間的隨機等級，20 到 30 之間的隨機加值\n><@t>5<@q>-</>10<@s>+</></> = 5 到 10 之間的隨機等級，維持目前加值\n><@t><@s>+</>20<@q>-</>30</> = 維持目前等級，20 到 30 之間的隨機加值\n><@t>{{max}}<@s>+</>{{max}}</> = 最大等級，最高加值\n><@t>{{quit_key}}</> = 退出\n>Input：\n\nmax_upgrade=最大升級上限： <@t>{max_base}<@s>+</>{max_plus}</>\n\ninvalid_upgrade_base=<@e>無效的等級：<@s>{base}</>\ninvalid_upgrade_base_random=<@e>無效的等級範圍：<@s>{min}</>-<@s>{max}</>\ninvalid_upgrade_plus=<@e>無效的加值：<@s>{plus}</>\ninvalid_upgrade_plus_random=<@e>無效的加值範圍：<@s>{min}</>-<@s>{max}</>\n\nremove_true_form_success=<@su>已成功移除第三型態</>\ntrue_form_success=<@su>已成功將貓咪進化為第三型態</>\n\nremove_success=<@su>已成功移除貓咪</>\nunlock_success=<@su>已成功解鎖貓咪</>\n\nunlock_cat_guide_success=<@su>已成功領取貓咪圖鑑獎勵</>\nremove_cat_guide_success=<@su>已成功取消領取貓咪圖鑑獎勵</>\n\nselect_cats_current=選擇目前已解鎖的貓咪\nselect_cats_not_unlocked=選擇尚未解鎖的貓咪\n\ntalents_version_warning=\n><@w>警告：編輯器的遊戲資料與此存檔的遊戲版本不符。本能可能無法如預期運作。\n>存檔版本：<@s>{save_version}</>\n>遊戲資料版本：<@s>{data_version}</>\n>如果遊戲資料過舊，應會在幾天內更新。</>\n\ntalents_success=<@su>已成功升級貓咪本能</>\ntalents_remove_success=<@su>已成功移除貓咪本能</>\ntalents_individual=個別編輯選取貓咪的本能\ntalents_all=將所有選取貓咪的本能升至滿級\nupgrade_talents_select_mod=選擇編輯貓咪本能的選項：\nno_talent_data=<@w>這隻貓咪沒有本能資料</>\ntalents=本能\nupgrade_talent_cats=升級貓咪本能\nforce_true_form_cats=強制進化為第三型態\nforce_true_form_cats_warning=\n><@w>警告：只有當你確定該貓咪擁有第三型態時才使用此功能，否則會導致第三型態出現錯誤。\n>此選項主要用途是編輯器的遊戲資料過舊，尚未新增更新的第三型態時。</>\n\nfilter_current_q=你想要只從目前已解鎖的貓咪中選擇 (<@t>1</>)，還是從所有貓咪中選擇 (<@t>2</>)？：\nselect_cats_currently_option=從你目前已解鎖的貓咪中選擇（例如，當選擇特定稀有度的貓咪時，只會選擇你目前擁有該稀有度的貓咪）\nselect_cats_all_option=從所有貓咪中選擇\n\nunlock_remove_cats=解鎖貓咪 / 移除貓咪\ntrue_form_remove_form_cats=進化為第三型態 / 移除第三型態\nupgrade_talents_remove_talents_cats=升級本能 / 移除本能\nunlock_remove_cat_guide=領取 / 取消領取貓咪圖鑑獎勵\n\nunlock_remove_q=你想要<@t>解鎖</>還是<@t>移除</>貓咪？：\ntrue_form_remove_form_q=你想要將貓咪<@t>進化為第三型態</>還是<@t>移除第三型態</>？：\nupgrade_talents_remove_talents_q=你想要<@t>升級</>還是<@t>移除</>本能？：\nunlock_cat_guide_remove_guide_q=你想要<@t>領取</>還是<@t>取消領取</>貓咪圖鑑獎勵？：\n\nfourth_form_remove_form_cats=超進化貓咪 (第四型態) / 移除貓咪第四型態\nforce_fourth_form_cats=強制超進化貓咪 (第四型態)\nfourth_form_success=<@su>已成功將貓咪超進化 (第四型態)</>\nremove_fourth_form_success=<@su>已成功移除貓咪第四型態</>\nfourth_form_cats=超進化貓咪 (第四型態)\nremove_fourth_form_cats=移除貓咪第四型態\nfourth_form_remove_form_q=你想要將貓咪<@t>超進化</>還是<@t>移除第四型態</>？：\nforce_fourth_form_cats_warning=\n><@w>警告：只有當你確定該貓咪擁有第四型態 (超進化) 時才使用此功能，否則會導致第四型態出現錯誤。\n>此選項的主要用途是當編輯器的遊戲資料過舊，且尚未新增新的型態時。</>\n\ngatya_info_progress=正在下載轉蛋資訊 (<@t>{current}</>/<@t>{total}</>)\nunknown_banner=未知的轉蛋系列\nbanner_txt={name} (<@s>{int}</>)\nfilter_down_q_gatya=你要從清單中移除重複及未知的轉蛋系列嗎？({{y/n}})：\n\nselect_cats_non_gatya=選擇非轉蛋貓咪\nfinished_cats_selection=你已經完成貓咪選擇了嗎？({{y/n}})：\n\ndownloading_cat_names=<@su>正在從 <@s>{url}</> 下載貓咪名稱</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/enemy.properties",
    "content": "total_selected_enemies=目前已選取 <@t>{total}</> 種敵人\nunlock_enemy_guide_success=<@su>已成功解鎖敵人圖鑑項目</>\nremove_enemy_guide_success=<@su>已成功移除敵人圖鑑項目</>\nselected_enemy=已選取 <@t>{name}</> (<@t>{id}</>)\nselect_enemies_valid=選擇敵人圖鑑中的所有敵人\nselect_enemies_invalid=選擇不在敵人圖鑑中的所有敵人\nselect_enemies_all=選擇所有敵人\nselect_enemies_id=依 ID 選擇敵人\nselect_enemies_name=依名稱選擇敵人\nselect_enemies=選擇敵人：\nenter_enemy_ids=你可以在這裡找到敵人 ID：<@t>https://battlecats.miraheze.org/wiki/Enemy_Release_Order</>\\n輸入敵人 ID {{range_input}}：\nenter_enemy_name=輸入敵人名稱：\nenemy_not_found_name=<@w>找不到名稱為 <@s>{name}</> 的敵人</>\nunlock_enemy_guide=解鎖敵人圖鑑\nremove_enemy_guide=移除敵人圖鑑\nenemy_guide=敵人圖鑑\nedit_enemy_guide=輸入選項以編輯敵人圖鑑項目："
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/fixes.properties",
    "content": "fix_gamatoto_crash=修復加碼多多造成的遊戲閃退\nfix_time_errors=修復時間相關錯誤\n\nfix_ototo_crash=修復城堡寶開發隊造成的遊戲閃退\n\nfix_gamatoto_crash_success=<@su>已成功修復加碼多多造成的遊戲閃退</>\nfix_time_errors_success=<@su>已成功修復時間相關錯誤 <@w>(兩台裝置時間都必須正確此功能才生效)</></>\nfix_ototo_crash_success=<@su>已成功修復城堡寶開發隊造成的遊戲閃退</>\n\nfixes=錯誤修復\n\nunlock_equip_menu=解鎖隊伍編成選單\nequip_menu_unlocked=<@su>已成功解鎖隊伍編成選單</>"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/gambling.properties",
    "content": "reset_wildcat_slots=<@su>已成功重置貓咪拉霸機</>\nreset_cat_scratcher=<@su>已成功重置貓咪刮刮樂</>\nreset_gambling_events=重置貓咪拉霸機與貓咪刮刮樂\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/gamototo.properties",
    "content": "enter_raw_gamatoto_xp=輸入加碼多多經驗值\nenter_gamatoto_level=輸入加碼多多等級\nedit_gamatoto_level_q=輸入選項以編輯加碼多多等級：\ngamatoto_xp=加碼多多經驗值\ngamatoto_level=加碼多多等級\ngamatoto_level_success=<@su>已成功將加碼多多等級設定為 <@s>{level}</> (XP：<@s>{xp}</>)</>\ngamatoto_level_current=<@t>目前的加碼多多等級為 <@q>{level}</> (XP：<@q>{xp}</>)</>\ngamatoto_xp_level=加碼多多經驗值 / 等級\n\ncurrent_gamatoto_helpers=目前的隊員：\ngamatoto_helper=隊員：<@t>{name}</> (稀有度：<@t>{rarity_name}</>)\n\nnew_gamatoto_helpers=新的隊員：\ngamatoto_helpers=加碼多多隊員\n\nototo_cat_cannon=城堡寶開發隊_貓咪城\n\ncurrent_cannon_stats=目前的貓砲狀態：\n\ncannon_part=<@t><@q>{name}</>{buffer}(等級 <@s>{level}</>)</>\ndevelopment={buffer}(開發進度：<@q>{development}</>)\ncannon_stats={parts}\n\nfoundation=基座\nstyle=裝飾\neffect=主炮\nimproved_foundation=強化基座\nimproved_style=強化裝飾\n\nunknown_stage=未知階段 (<@s>{stage}</>)\n\nselected_cannon=<@t>已選取的貓砲：<@q>{name}</></>\nselected_cannon_stage=<@t>貓砲：<@q>{name}</> 目前階段：<@q>{stage}</></>\n\ncannon_edit_type=你想要個別編輯每座貓砲，還是將變更套用至所有已選的貓砲？：\n\ncannon_dev_level_q=你想要編輯貓砲的開發進度還是等級？：\ndevelopment_o=開發進度\nlevel_o=等級\n\nselect_development=選擇開發階段：\nselect_cannon=選擇貓砲\ncannon_level=貓砲等級\n\ncannon_success=<@su>已成功編輯城堡開發隊貓砲</>\n\ncat_shrine=編輯貓咪神社\nshrine_level=編輯神社等級\nshrine_xp=神社 XP (經驗值)\ncurrent_shrine_xp_level=<@t>目前的 XP：<@q>{xp}</> (等級：<@q>{level}</>)</>\ncat_shrine_choice_dialog=你想要做什麼？：\nshrine_level_dialog=輸入貓咪神社等級（最大值：<@q>{max_level}</>）：\nshrine_xp_dialog=輸入貓咪神社 XP（最大值：<@q>{max_xp}</>）：\ncat_shrine_edited=<@su>已成功編輯貓咪神社</>\nmake_catshrine_appear=在遊戲中顯示貓咪神社\nmake_catshrine_disappear=在遊戲中隱藏貓咪神社\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/gatya.properties",
    "content": "event_tickets=活動轉蛋券 / 招福轉蛋券\ndownloading_gatya_data=正在下載轉蛋活動資料...\ndownload_gatya_data_success=<@su>已成功下載轉蛋活動資料</>\ndownload_gatya_data_fail=<@e>下載轉蛋活動資料失敗。請再試一次</>\nsave_gatya_error=<@e>因 {error} 導致儲存轉蛋資料失敗</>\ngatya_by_id_q=你想要依 <@t>ID</> 還是 <@t>名稱</> 來選擇轉蛋系列？：\nby_id=依 ID\nby_name=依名稱\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/gold_pass.properties",
    "content": "gold_pass_dialog=輸入你想要的 <@t>會員 ID</>（留 <@q>白</> 則產生 <@q>隨機</> ID，或者輸入 <@q>-1</> 來 <@q>移除</> 黃金會員）：\ngold_pass=黃金會員 / 貓咪俱樂部\ngold_pass_remove_success=<@su>已成功移除黃金會員</>\ngold_pass_get_success=<@su>已成功獲取黃金會員 (ID：<@t>{id}</>)</>。<@w>注意：如果遊戲連線後發現實際沒有購買，可能會自動移除黃金會員。這是遊戲本身的防護機制，我無法修復此問題，因此請不要將其作為 bug 回報。</>\nofficer_pass_fixed=<@su>已成功修復貓咪俱樂部導致的閃退問題</>\nfix_officer_pass_crash=修復貓咪俱樂部閃退"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/items.properties",
    "content": "# Note that not all items are here\n\ncatamins=喵力達\ncatfruit=貓薄荷\nbase_materials=城堡開發材料\ninquiry_code=詢問碼\nrare_gatya_seed=稀有轉蛋種子碼\nnormal_gatya_seed=一般轉蛋種子碼\nevent_gatya_seed=活動轉蛋種子碼\nunlocked_slots=解鎖出陣列表|編成欄位\npassword_refresh_token=密碼重新整理權杖\nchallenge_score=挑戰賽分數\ndojo_score=貓咪道場分數\nitems=道具\nuser_rank_rewards=領取等級排行獎勵（不會實際領取獎勵）\n\ncatfood=貓罐頭\nxp=XP(經驗值)\nnormal_tickets=貓咪轉蛋券|基本轉蛋券|銀券\nrare_tickets=稀有券|金券\nplatinum_tickets=白金券\nlegend_tickets=傳說券\n100_million_tickets=一億次下載紀念券|一億下載紀念券\n100_million_warn=<@w>注意：只有在「一億下載紀念轉蛋」活動進行期間，才能看到並使用這些轉蛋券</>\nplatinum_shards=白金券碎片\nnp=NP\nleadership=首領旗\ncatseyes=貓眼石\nbattle_items=戰鬥道具\nduration=<@t>{days}</t> 天，<@t>{hours}</t> 小時，<@t>{minutes}</> 分鐘，<@t>{seconds}</> 秒\nendless_item_item=<@s>{item}</>：<@s>{int}</>\nendless_items_success=<@su>已成功編輯無限道具</>\ninvalid_minute_count=<@e>無效的分鐘數</>\nenter_duration_minutes=輸入無限道具持續的分鐘數（如果輸入 <@t>infinity</> 則道具將永久有效）：\ninfinity_duration=<@t>infinity</>\ninfinity=無限\nenter_duration_minutes_item=輸入無限 <@t>{item}</> 持續的分鐘數（如果輸入 <@t>infinity</> 則道具將永久有效）：\nbattle_items_endless=無限戰鬥道具\ntalent_orbs=本能玉\nscheme_items=獸石 / 獸結晶\nlabyrinth_medals=地底迷宮獎章\nrestart_pack=回歸禮包\nengineers=城堡開發隊員\ngamototo=加碼多多 / 城堡寶開發隊\nspecial_skills=特殊能力 / 貓炮\ntreasure_chests=神秘寶箱\nunknown_treasure_chest_name=未知的寶箱 ({id})\n\nrare_ticket_trade=稀有券兌換\nrare_ticket_trade_feature_name=稀有券兌換 (可獲得不會被封帳的稀有券)\n\nother=其他\ngatya=轉蛋\nlevels=關卡 / 傳奇故事 / 寶物\ncats_special_skills=貓咪 / 特殊能力\n\ngatya_item_unknown_name=未知的道具\nunknown_catamin_name=未知的喵力達 <@t>{id}</>\nunknown_catseye_name=未知的貓眼石 <@t>{id}</>\nunknown_catfruit_name=未知的貓薄荷 <@t>{id}</>\nunknown_labyrinth_medal_name=未知的地底迷宮獎章 <@t>{id}</>\n\nreset_golden_cat_cpus_success=<@su>已成功重置跳過戰鬥的使用次數</>\nreset_golden_cat_cpus=重置跳過戰鬥使用次數\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/map.properties",
    "content": "tutorial_already_cleared=<@w>你已經通過教學關卡</>\ntutorial_cleared=<@su>已成功通過教學關卡</>\nclear_tutorial=通過教學關卡\n\nclear_stages=通關關卡\nunclear_stages=取消通關關卡\nclear_unclear_q=你要<@t>通關</>還是<@t>取消通關</>關卡？：\n\nadd_enigma_stages=新增發掘關卡\nclear_enigma_stages=通關發掘關卡\ncurrent_enigma_stages=目前的發掘關卡：\nenigma_stage=發掘關卡 <@q>{name}</> (ID：<@q>{id}</>) \nunknown_enigma_name=未知的發掘關卡名稱 (ID：<@q>{id}</>)\nenigma_select=選擇要新增的發掘關卡\nenigma_success=<@su>已成功新增發掘關卡</>\nwipe_enigma=你要清除目前的發掘關卡嗎？({{y/n}})：\naku_realm_unlocked=<@su>已成功解鎖魔界篇</>\nunlock_aku_realm=解鎖魔界篇\n\nselect_story_chapters=選擇傳奇故事章節\nchapter_progress_txt=（例如：<@q>0</> = 未通關任何關卡，<@q>1</> = 通關第一關，<@q>2</> = 通關第一和第二關，... <@q>{max}</> = 全數通關）\nedit_chapter_progress_all=輸入進度以將每個章節設定為 {{chapter_progress_txt}}：\nedit_chapter_progress=輸入進度以將 <@t>{chapter_name}</> 設定為 {{chapter_progress_txt}}：\nedit_stage_clear_count=輸入關卡通關次數：\nstory_cleared=<@su>已成功通關傳奇故事章節</>\nindividual_chapters=個別章節\nall_chapters=所有章節\nindividual_chapters_dialog=你要<@t>個別</>編輯每個章節的通關進度？還是將<@t>所有</>章節設定為相同的進度？：\nindividual_clear_counts=個別通關次數\nall_clear_counts=統一通關次數\nindividual_clear_counts_dialog=你要<@t>個別</>編輯每個關卡的通關次數？還是將<@t>所有</>關卡設定為相同的通關次數？：\nclear_story=主線故事章節|通關故事章節\n\nmap_name_star={name} {star} 皇冠\n\nclear=通關\nunclear=取消通關\n\noutbreaks=不死生物襲擊 / 殭屍關卡\n\nclear_unclear_outbreaks=你要<@t>通關</>還是<@t>取消通關</>不死生物襲擊？：\nclear_outbreaks_success=<@su>已成功通關不死生物襲擊</>\nunclear_outbreaks_success=<@su>已成功取消通關不死生物襲擊</>\nno_valid_outbreaks=<@e>錯誤：找不到有效的不死生物襲擊</>\n\naku_chapters=魔界篇章節\naku_clear_success=<@su>已成功通關魔界篇</>\naku_current_stage=魔界篇關卡 <@q>{name}</> (ID：<@q>{id}</>)\n\nitf_timed_scores=未來篇時間積分\nitf_timed_scores_dialog=你要編輯<@t>整個章節</>的分數，還是<@t>個別關卡</>的分數？\nitf_timed_scores_edited=<@su>已成功編輯未來篇時間分數</>\nitf_timed_score_dialog=輸入時間分數：\ncurrent_stage={chapter_name} <@t>{stage_name}</>\nitf_timed_scores_individual_dialog=你要<@t>個別</>編輯每個選取關卡的時間分數？還是將<@t>所有</>選取的關卡設定為相同的時間分數？：\n\nfilibuster_stage_reclearing_allowed=<@su>已成功重啟議事阻撓貓關卡</>\nfilibuster_reclearing=重啟議事阻撓貓關卡\n\nall_selected_stages=所有選取的關卡\n\nunknown_map_name=未知的地圖名稱 (ID：<@q>{id}</>)\nmap_name={name} <@s>(ID：<@q>{id}</>)</>\nedit_map_chapters=選擇章節\n\nclear_whole_chapters=通關整個章節\nunclear_whole_chapters=取消通關整個章節\n\nclear_specific_stages=通關特定關卡\nunclear_specific_stages=取消通關特定關卡\n\nselect_clear_type=你要<@t>通關整個章節</>還是<@t>通關特定關卡</>？：\nselect_unclear_type=你要<@t>取消通關整個章節</>還是<@t>取消通關特定關卡</>？：\n\ncustom_star_count_per_chapter_yn=你要為每個章節設定自訂的星數/皇冠數嗎？({{y/n}})：\nmodify_clear_amounts=正在將每個選取關卡的通關次數設定為 <@t>1</>。你要更改此設定嗎？({{y/n}})：\nclear_amount_chapter=為每個選取的章節設定不同的通關次數\nclear_amount_all=為所有選取的章節設定相同的通關次數\nclear_amount_stages=為每個選取的關卡設定不同的通關次數\nselect_clear_amount_type=輸入你要使用的通關次數設定模式：\nclear_amount_enter=輸入通關次數：\ncustom_star_count_per_chapter=輸入星數/皇冠數（最大值 <@q>{max}</>）：\n\ncustom_star_count_per_chapter_unclear=\n>輸入要移除的星數/王冠數：\n><@s><@t>1</> = 整張地圖取消通關</>\n><@s><@t>2</> = 從第 2、3、4 冠地圖中取消通關</>\n><@s><@t>3</> = 從第 3、4 冠地圖中取消通關</>\n><@s><@t>4</> = 從第 4 冠地圖中取消通關</>\n>(最大值 <@q>{max}</>)：\n\ncurrent_sol_chapter=章節 <@t>{name}</> (ID：<@q>{id}</>)\ncurrent_sol_star=星數/王冠：<@q>{star}</>\ncurrent_sol_stage=關卡 <@q>{name}</> (ID：<@q>{id}</>)\nmap_chapters_edited=<@su>已成功編輯章節</>\nsol=傳奇故事\nevent=一般活動關卡\ncollab=合作活動關卡\nselect_map=選擇地圖\nselect_map_dialog=\n>選擇你要編輯的地圖\n>你可以輸入一個數字範圍（例如 <@q>1-5</>）、個別數字（例如 <@q>1 3 5</>），或兩者混用（例如 <@q>1-3 5</>）\n>你也可以輸入地圖名稱或部分名稱（例如 <@t>{example}</>）來進行搜尋/選擇\n>你也可以輸入 <@q>all</> 來選擇所有章節\n>輸入：\nno_map_found=<@e>找不到名稱為 <@s>{name}</> 的地圖</>\nfinished_selecting_maps=你完成地圖選擇了嗎？({{y/n}})：\ncurrent_maps=目前的地圖：\n\nselect_stage=選擇關卡\n\ngauntlets=強襲關卡\ncollab_gauntlets=合作強襲關卡\nuncanny=真傳奇故事\ncatamin_stages=喵力達關卡\nbehemoth_culling=超獸討伐關卡\nlegend_quest=傳奇尋寶記\ntowers=風雲貓咪塔\nzero_legends=傳奇故事0\n\nunclear_other_stages=你要覆寫章節中目前的進度嗎？({{y/n}}) <@t>n</> = 僅更改選取關卡的通關次數，<@t>y</> = 取消通關該章節中較後的已通關關卡：\n\nselect_stage_progress=輸入要通關到哪一關為止（包含該關卡）：\n\nzero_legends_warning=<@w>警告：如果你使用的遊戲版本還沒有「傳奇故事0」地圖，嘗試將其編輯為通關將會導致遊戲閃退！</>\n\nstages_select=輸入數字 {{range_input}}\n\nchange_clear_amount_catamin=更改章節通關次數\nclear_unclear_stage_catamin=通關 / 取消通關喵力達關卡\ncatamin_stage_clear_q=你想<@t>更改喵力達專用關卡的通關次數</>，還是只想<@t>通關或取消通關關卡</>？：\n\nselect_map_from_names=選擇地圖\n\nenter_clear_amount_catamin_map=輸入章節 <@t>{name}</> (ID：<@t>{id}</t>) 的通關次數（<@t>0</> = 尚未通關此章節，<@t>3</> 或以上代表該章節會消失）：\nenter_clear_amount_catamin=輸入選取章節的通關次數（<@t>0</> = 尚未通關該章節，<@t>3</> 或以上代表該章節會消失）：\n\ncatamin_stage_success=<@su>已成功編輯喵力達關卡</>\n\ncatamin_clear_amounts_q=你要<@t>個別</>編輯每個章節的通關次數，還是<@t>一次全部設定</>？：\n\ndojo_catclaw_championships=通關貓咪道場晉段測驗\n\nfinished=完成\nedit_chapters_q=你要編輯什麼？：\n\nclear_whole_chapter=通關整個章節\nclear_to_specific_stage=通關特定關卡\nclear_whole_q=你要<@t>通關整個章節</>還是<@t>通關章節內特定關卡</>？：\n\nclear_all=通關所有章節\nhandle_individually=個別編輯每個章節\n\nclear_chapters_q=你要<@t>通關所有選取的章節</>還是<@t>個別編輯每個章節的進度</>？：\nmax_stars=輸入最大皇冠數（最大值：<@t>{max}</>）：\n\nedit_map_all=所有章節統一通關次數\nedit_chapters_q_all=你要<@t>為所有選取的章節設定相同的通關次數</>還是<@t>個別</>編輯每個章節的通關次數？\n\nedit_whole_chapter=為整個章節設定相同的通關次數\nedit_specific_stages=編輯特定關卡的通關次數\nedit_chapter_q=你要<@t>為章節內的所有關卡設定相同的通關次數</>還是<@t>編輯特定關卡的通關次數</>？：\n\neach_stage_individually=為每個選取的關卡設定不同通關次數\nstage_all_at_once=為所有選取的關卡設定相同通關次數\nset_clear_count_stage_q=你要<@t>為每個選取的關卡設定不同通關次數</>還是<@t>為所有選取的關卡設定相同通關次數</>？：\n\nunknown_stage_name=未知的關卡名稱 (索引：<@t>{index}</>)\ncurrent_stage_map=<@t>{name} <@s>(索引：<@q>{index}</>)</></>\n\nedit_progress_clear=編輯地圖進度\nedit_progress_unclear=編輯地圖進度 (可取消通關)\nedit_clear_counts=編輯關卡通關次數\n\nkeep_selecting=繼續選擇\nremove_selection=移除選擇\nfinish_selection=完成選擇\nmap_selection_q=你想要做什麼？"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/medals.properties",
    "content": "medals=貓咪獎章\nadd_medals=新增獎章\nremove_medals=移除獎章\nmedal_add_remove_dialog=你希望 <@t>新增獎章</> 或 <@t>移除獎章</>？：\nmedal_string={medal_name}： <@q>{medal_req}</>\nselect_medals=選擇獎章：\nmedals_added=<@su>成功新增貓咪獎章</>\nmedals_removed=<@su>成功移除貓咪獎章</>"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/missions.properties",
    "content": "missions=貓咪任務\ncomplete_reward=完成任務但不領取獎勵\ncomplete_claim=完成任務並領取獎勵\nuncomplete=取消完成任務\nselect_mission_claim=你要<@t>完成任務但不領取獎勵</>、<@t>完成任務並領取獎勵 <@q>(不會真的收到獎勵道具)</></>，還是<@t>取消完成任務(如果可行的話)</>？\nselect_missions=選擇要編輯的任務：\nmissions_edited=<@su>已成功編輯任務</>"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/playtime.properties",
    "content": "playtime_str=<@t>{hours}</> 小時, <@t>{minutes}</> 分鐘, <@t>{seconds}</> 秒 (<@t>{frames} </>幀)\nplaytime_current=目前的遊戲時數：{{playtime_str}}\nplaytime_edited=已成功將遊戲時數編輯為 {{playtime_str}}\nplaytime_hours_prompt=輸入要設定的遊戲時數<@t>小時</>數：\nplaytime_minutes_prompt=輸入要設定的遊戲時數<@t>分鐘</>數：\nplaytime_seconds_prompt=輸入要設定的遊戲時數<@t>秒</>數：\nplaytime=遊戲時數"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/scheme_items.properties",
    "content": "scheme_items_edit_success=<@su>已成功編輯獸石/獸結晶</>\nscheme_items_select_gain=選擇要獲取的獸石/獸結晶\nscheme_items_select_remove=選擇要移除的獸石/獸結晶\ngain_remove_scheme_items=你想要<@t>獲取</>還是<@t>移除</>獸石/獸結晶？：\ngain_scheme_items=獲取獸石/獸結晶\nremove_scheme_items=移除獸石/獸結晶"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/special_skills.properties",
    "content": "special_skills_dialog=選擇要升級的基地能力\nupgrade_individual_skill=為每個選取的能力個別輸入等級\nupgrade_all_skills=輸入等級數值以套用至所有選取的能力\n\nupgrade_skills_select_mod=選擇升級能力的選項：\n\nselected_skill=已選取 <@t>{name}</>\nselected_skill_upgrades={{selected_skill}}：<@t>{base_level}<@s>+</>{plus_level}</>\nselected_skill_upgraded=<@t>{name}</> 已升級至 <@t>{base_level}<@s>+</>{plus_level}\nskills_edited=<@su>已成功編輯特殊能力</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/talent_orbs.properties",
    "content": "total_current_orbs=目前本能玉總數：<@q>{total_orbs}</>\ntotal_current_orb_types=目前本能玉種類總數：<@q>{total_types}</>\ncurrent_orbs=目前的本能玉：\norb_select=選擇要編輯的本能玉：\nselected_orbs=已選取的本能玉：\nedit_orbs_individually=你要個別編輯每顆本能玉 (<@q>1</>) 還是一次全部編輯 (<@q>2</>)？：\nedit_orbs_all=輸入數值以套用至所有選取的本能玉（最大值 <@t>{max}</>）：\nfailed_to_load_orbs=無法載入本能玉\n\nedit_orbs_help=\n>說明：\n>可用等級：{all_grades_str}\n>可用屬性：{all_attributes_str}\n>可用效果：{all_effects_str}\n><@w>注意：並非所有等級和效果都適用於所有屬性。</>\n>輸入範例：\n>    <c>aku</> - 選擇<c>所有惡魔</>本能玉\n>    <r>red</> <b>s</> - 選擇<r>所有紅色</> <b>S</> 級本能玉\n>    <b>alien</> <r>d</> <r>0</> - 選擇可提升<r>攻擊力</>的 <b>D</> 級<b>異星</>本能玉。\n>    <o>c</> <dm>1</> - 選擇 <o>C</> 級的傳奇故事強化本能玉\n>如果你想選擇<@q>所有</>本能玉，請輸入：\n>    <@q>*</>\n>如果你想進行<@q>多重選擇</>，請使用<@q>逗號</>分隔，例如：\n>    <b>s</> <bl>black</> <g>4</>,<r>d</> <o>3</>,<g>floating</>\n>\n\n"
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/treasures.properties",
    "content": "whole_chapters=整個章節\nindividual_stages=個別關卡\ntreasure_groups=寶物群組 / 寶物區域\ntreasure_dialog=你要編輯<@t>整個章節</>、<@t>個別關卡</>還是個別<@t>寶物群組</>的寶物？：\ntreasures_edited=<@su>已成功編輯寶物</>\nper_chapter=個別章節\nall_selected_chapters=所有選取的章節\nedit_per_chapter=你要一次編輯<@t>所有選取的章節</>，還是<@t>個別編輯每個章節</>的資料？：\nno_treasure=無寶物 (尚未獲得)\ncustom_treasure_level=自訂寶物等級 (<@w>請確定你知道自己在做什麼再進行編輯！</>)\ntreasure_level_dialog=輸入你要設定的寶物等級：\ncustom_treasure_level_dialog=輸入你要設定的自訂寶物等級：\nselect_stage_by_id=依 ID 選擇關卡\nselect_stage_by_name=依名稱選擇關卡\nselect_stage_dialog=你想要依 <@t>ID</> 還是 <@t>名稱</> 來選擇關卡？：\nselect_stage_id=輸入你要選擇的關卡 ID {{range_input}}\nselect_stages_name=選擇關卡：\nselect_treasure_groups=選擇你要編輯的寶物群組：\nstory_treasures=故事章節寶物\ncurrent_chapter=目前章節：<@t>{chapter_name}</>\ncurrent_treasure_group=目前寶物群組：<@t>{treasure_group_name}</>\ngroup_individual=個別群組\ngroup_all_at_once=所有選取的群組\nselect_treasure_groups_individual=你要<@t>個別</>編輯每個寶物群組的寶物等級，還是<@t>一次編輯所有選取的群組</>？："
  },
  {
    "path": "src/bcsfe/files/locales/tw/edits/user_rank.properties",
    "content": "claim=領取\nunclaim=取消領取\nfix_claimed=修復領取狀態\nclaim_or_unclaim_ur=你想要<@t>領取</>、<@t>取消領取</>還是<@t>修復領取狀態 (取消領取任何高於目前等級排行的獎勵)</>等級排行獎勵？：\nselect_ur=選擇等級獎勵\nur_claimed_success=<@su>成功領取等級排行獎勵</>\nur_unclaimed_success=<@su>成功取消領取等級排行獎勵</>\nur_string=等級排行：<@s>{rank}</>：{description}\nur_fix_claimed_success=<@su>成功修復已領取的用戶等級獎勵狀態</>"
  },
  {
    "path": "src/bcsfe/files/locales/tw/metadata.json",
    "content": "{\n  \"name\": \"繁體中文 (Traditional Chinese)\",\n  \"authors\": [\"LinYuAn\"]\n}\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/config.properties",
    "content": "# filename=\"config.properties\"\r\nconfig=Cấu hình\r\nedit_config=Chỉnh sửa cấu hình\r\ndefault_value=(giá trị mặc định: <@q>{default_value}</>)\r\ncurrent_value=(giá trị hiện tại: <@q>{current_value}</>)\r\nconfig_value_txt=<@s>{{current_value}} {{default_value}}</>\r\n\r\nconfig_dialog=Chọn một tùy chọn cấu hình để chỉnh sửa:\r\n\r\nupdate_to_beta_desc=Kiểm tra cập nhật cho phiên bản beta {{config_value_txt}}\r\nupdate_to_beta=Cập nhật lên phiên bản beta\r\n\r\nshow_update_message_desc=Hiển thị thông báo khi có phiên bản mới {{config_value_txt}}\r\nshow_update_message=Hiển thị thông báo cập nhật\r\n\r\nconfig_full=<@t>{key_desc}</>\r\n\r\ndisable_maxes_desc=Tắt giá trị tối đa khi chỉnh sửa {{config_value_txt}}\r\ndisable_maxes=Tắt giá trị tối đa\r\n\r\nmax_backups_desc=Số lượng tối đa sao lưu save file giữ {{config_value_txt}}\r\nmax_backups=Sao lưu save tối đa\r\n\r\navailable_themes=Các chủ đề khả dụng:\r\ntheme_desc=Chủ đề sử dụng {{config_value_txt}}\r\ntheme=Chủ đề\r\n\r\nshow_missing_locale_keys=Hiển thị các khóa ngôn ngữ thiếu\r\nshow_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}}\r\n\r\nreset_cat_data_desc=Đặt lại tất cả dữ liệu cat khi xóa cat khỏi save file {{config_value_txt}}\r\nreset_cat_data=Đặt lại dữ liệu cat khi xóa cat\r\n\r\nfilter_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}}\r\nfilter_current_cats=Lọc cat hiện tại khi chọn cat\r\n\r\nset_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}}\r\nset_cat_current_forms=Đặt hình dạng hiện tại của cat khi mở khóa hình dạng\r\n\r\nstrict_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}}\r\nstrict_upgrade=Kiểm tra nâng cấp nghiêm ngặt\r\n\r\nseparate_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}}\r\nseparate_cat_edit_options=Tách tùy chọn chỉnh sửa cats\r\n\r\nstrict_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}}\r\nstrict_ban_prevention=Ngăn chặn cấm nghiêm ngặt\r\n\r\nmax_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}}\r\nmax_request_timeout=Thời gian chờ yêu cầu tối đa\r\n\r\ngame_data_repo_desc=Kho sử dụng cho dữ liệu trò chơi {{config_value_txt}}\r\ngame_data_repo=Kho dữ liệu trò chơi\r\ngame_data_repo_dialog=Nhập kho dữ liệu trò chơi để sử dụng:\r\n\r\nforce_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}}\r\nforce_lang_game_data=Bắt buộc sử dụng dữ liệu trò chơi cho ngôn ngữ hiện tại\r\n\r\nclear_tutorial_on_load_desc=Xóa tutorial khi tải save file vào trình chỉnh sửa {{config_value_txt}}\r\nclear_tutorial_on_load=Xóa tutorial khi tải save\r\n\r\nremove_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}}\r\nremove_ban_message_on_load=Xóa thông báo cấm khi tải save\r\n\r\nunlock_cat_on_edit_desc=Mở khóa cat khi chỉnh sửa level, talents, form, v.v. {{config_value_txt}}\r\nunlock_cat_on_edit=Mở khóa cat khi chỉnh sửa\r\n\r\nuse_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}}\r\nuse_file_dialog=Sử dụng hộp thoại tệp\r\n\r\nadb_path_desc=Đường dẫn đến executable adb {{config_value_txt}}\r\nadb_path=Đường dẫn ADB\r\n\r\nuse_waydroid=Sử dụng shell waydroid thay vì adb\r\nuse_waydroid_desc=Waydroid không hỗ trợ adb root, vì vậy sử dụng shell waydroid thay thế {{config_value_txt}}\r\n\r\nignore_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}}\r\nignore_parse_error=Bỏ qua lỗi phân tích lưu\r\n\r\nstring_config_dialog=Nhập giá trị mới cho <@q>{val}</>:\r\n\r\n\r\nenable_disable_dialog=Bạn muốn <@q>bật</> hay <@q>tắt</> tính năng này?:\r\n\r\n\r\nenable=Bật\r\ndisable=Tắt\r\n\r\nenabled=Đã bật\r\ndisabled=Đã tắt\r\n\r\nconfig_success=<@su>Đã cập nhật cấu hình thành công</>\r\n\r\nyaml_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?\r\n\r\nuse_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}}\r\nuse_pkexec_waydroid=Sử dụng binary pkexec để chạy các lệnh waydroid\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/files.properties",
    "content": "# filename=\"files.properties\"\r\nanother_path=Nhập đường dẫn thủ công\r\nselect_files_dir=Chọn tệp trong thư mục:\r\nenter_path=Nhập đường dẫn tệp / vị trí:\r\nenter_path_dir=Nhập đường dẫn thư mục / vị trí:\r\nenter_path_default=Nhập đường dẫn tệp / vị trí (mặc định: <@t>{default}</>):\r\ncurrent_files_dir=Các tệp hiện tại trong thư mục <@t>{dir}</>:\r\nother_dir=Nhập thư mục khác\r\nno_files_dir=<@e>Không có tệp trong thư mục</>\r\npath_not_exists=<@e>Đường dẫn không tồn tại</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/input.properties",
    "content": "# filename=\"input.properties\"\r\ninput_int=Nhập một số giữa <@q>{min}</> và <@q>{max}</>:\r\nselect_edit=Chọn các tùy chọn cho <@t>{group_name}</>:\r\ninput_int_default=Nhập một số giữa <@q>{min}</> và <@q>{max}</> (mặc định <@q>{default}</>):\r\ninput_many=Nhập các số giữa <@q>{min}</> và <@q>{max}</> cách nhau bằng dấu cách:\r\ninput_single=Nhập một số giữa <@q>{min}</> và <@q>{max}</>:\r\ninput=Nhập giá trị cho <@t>{name}</> (giá trị hiện tại: <@q>{value}</>) (giá trị tối đa: <@q>{max}</>):\r\ninput_min=Nhập giá trị cho <@t>{name}</> (giá trị hiện tại: <@q>{value}</>) (phạm vi: <@q>{min}</> - <@q>{max}</>):\r\ninput_non_max=Nhập giá trị cho <@t>{name}</> (giá trị hiện tại: <@q>{value}</>):\r\ninput_all=Nhập giá trị cho tất cả <@t>{name}</> (giá trị tối đa: <@q>{max}</>):\r\nvalue_changed=<@su>Đã thay đổi thành công <@s>{name}</> thành <@s>{value}</>\r\nvalue_gave=<@su>Đã cung cấp thành công <@s>{name}</>\r\nall_at_once=Chọn tất cả các tùy chọn cùng lúc\r\ninvalid_input=<@e>Đầu vào không hợp lệ. Vui lòng thử lại.</>\r\ninvalid_input_int=<@e>Đầu vào không hợp lệ. Vui lòng nhập số giữa <@s>{min}</> và <@s>{max}</></>\r\nfeatures=Tính năng:\r\ngo_back=Quay lại\r\nyes_key=y\r\nquit_key=q\r\nrange_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</>:\r\nselect_features=\r\n>Để chọn một tính năng, nhập\r\n>- một <@q>số</> tương ứng với số bên trái\r\n>- <@t>văn bản</> để tìm kiếm tính năng\r\n>Bạn có thể nhấn <@t>enter</> để xem tất cả tính năng\r\n>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</>\r\n>Đầu vào:\r\n\r\nindividual=Cá nhân\r\nedit_all_at_once=Tất cả cùng lúc\r\nfinish=Hoàn thành\r\nselect_option=Chọn tùy chọn:\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/locale.properties",
    "content": "# filename=\"locale.properties\"\r\navailable_locales=Các ngôn ngữ khả dụng:\r\nlocale_desc=Ngôn ngữ sử dụng {{config_value_txt}}\r\nlocale=Ngôn ngữ\r\nlocale_dialog=Chọn một ngôn ngữ:\r\nadd_locale=Thêm ngôn ngữ\r\nremove_locale=Xóa ngôn ngữ\r\nlocale_remove_dialog=Chọn các ngôn ngữ để xóa:\r\nenter_locale_git_repo=Nhập kho git của ngôn ngữ (ví dụ <@t>https://codeberg.org/fieryhenry/ExampleEditorLocale.git</>):\r\nlocale_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}}):\r\nlocale_added=<@su>Đã thêm ngôn ngữ thành công</>\r\nchecking_for_locale_updates=Đang kiểm tra cập nhật cho ngôn ngữ bên ngoài <@t>{locale_name}</>...\r\nexternal_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}}</>\r\nexternal_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></></>\r\ninvalid_git_repo=<@e>Kho git không hợp lệ</>\r\nlocale_cancelled=<@e>Đã hủy</>\r\nrestart_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\r\nlocale_changed=<@su>Đã thay đổi ngôn ngữ thành công thành <@t>{locale_name}</>.\\n{{restart_to_see_changes}}</>\r\nlocale_removed=<@su>Đã xóa ngôn ngữ thành công <@t>{locale_name}</>.\\n{{restart_to_see_changes}}</>\r\nno_external_locales=<@e>Không tìm thấy ngôn ngữ bên ngoài</>\r\n\r\nmissing_locale_keys=Các khóa ngôn ngữ thiếu:\r\nextra_locale_keys=Các khóa ngôn ngữ thêm:\r\n\r\nlocale_text=\r\n>Ngôn ngữ hiện tại: <@s>{locale_name}</> (Phiên bản: <@s>{locale_version}</>)\r\n>Được tạo bởi <@s>{locale_author}</>\r\n>Vị trí tệp ngôn ngữ: <@s>{locale_path}</>\r\n\r\ndefault_locale_text_authors=\r\n>Ngôn ngữ hiện tại: <@s>{name}</>\r\n>Được tạo bởi <@s>{authors}</>\r\n>Vị trí tệp ngôn ngữ: <@s>{path}</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/main.properties",
    "content": "# filename=\"main.properties\"\r\n# Full documentation: https://codeberg.org/fieryhenry/ExampleEditorLocale#format-of-the-properties-files\r\n# color formatting\r\n#\r\n# <@p> = primary color\r\n# <@s> = secondary color\r\n# <@t> = tertiary color\r\n# <@q> = quaternary color\r\n# <@e> = error color\r\n# <@w> = warning color\r\n# <@su> = success color\r\n#\r\n# </> = close current color\r\n# When coloring, use the above formatting tags, not the actual color codes / names so that different colors can be used for different themes.\r\n# You should only use the actual color codes / names if the exact colors are important, such coloring a target red talent orb as red.\r\n# If you want to write < or > or / in the text, escape them with a backslash (\\) e.g. \\< or \\> or \\/\r\n#\r\n# <#rrggbb> = hex color\r\n#\r\n# <w> = white\r\n# <bl> = black\r\n# <r> = red\r\n# <g> = green\r\n# <b> = blue\r\n# <y> = yellow\r\n# <m> = magenta\r\n# <c> = cyan\r\n# <dy> = dark yellow\r\n# <dg> = dark grey\r\n# <db> = dark blue\r\n# <dc> = dark cyan\r\n# <dm> = dark magenta\r\n# <dr> = dark red\r\n# <dgn> = dark green\r\n# <lg> = light grey\r\n# <o> = orange\r\n\r\ndownloading=<@su>Đang tải xuống <@s>{file_name}</> từ <@s>{pack_name}</> với phiên bản <@s>{version}</>\r\nfailed_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.</>\r\nno_device_error=<@e>Không tìm thấy thiết bị kết nối</>\r\nno_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.</>\r\nexit=Thoát\r\ntkinter_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.</>\r\ntkinter_not_found_enter_path_file=Vui lòng nhập đường dẫn/vị trí của tệp {initialfile}:\r\ntkinter_not_found_enter_path_file_save=Vui lòng nhập đường dẫn/vị trí để lưu tệp {initialfile}:\r\ntkinter_not_found_enter_path_dir=Vui lòng nhập đường dẫn/vị trí của thư mục {initialdir} thay thế:\r\ndiscord_url=https://discord.gg/DvmMgvn5ZB\r\n\r\nwelcome=\r\n><@t>Chào mừng đến với <@s>Trình chỉnh sửa save file Battle Cats</>!\r\n>Được tạo bởi <@s>fieryhenry</>\r\n>\r\n>Codeberg: <@s>https://codeberg.org/fieryhenry/BCSFE-Python</>\r\n>Discord: <@s>{{discord_url}}</> - Vui lòng báo lỗi đến <@s>#bug-reports</> và gợi ý đến <@s>#suggestions</>\r\n>Ủng hộ: <@s>https://ko-fi.com/fieryhenry</>\r\n>\r\n>Vị trí tệp cấu hình: <@s>{config_path}</>\r\n>\r\n>{theme_text}\r\n>\r\n>{locale_text}\r\n>\r\n><@q>Cảm ơn:\r\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/</>\r\n>- <@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\r\n>- 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</>\r\n>- 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}}</></>\r\n>\r\n><@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ở.</>\r\n>\r\n><@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.\r\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.\r\n>Nếu save file bị hỏng, vui lòng vẫn báo cáo trong discord.\r\n>Tôi khuyên bạn nên sao lưu save file trước khi chỉnh sửa.</>\r\n\r\nreport_message=Vui lòng báo cáo điều này đến <@s>#bug-reports</> trên discord: <@s>{{discord_url}}</>\r\nreport_message_l=Vui lòng báo cáo điều này đến <@s>#bug-reports</> trên discord: <@s>{{discord_url}}</>\r\ntry_again_message=Vui lòng thử lại. Nếu lỗi vẫn tiếp diễn {{report_message_l}}\r\nall=Tất cả\r\n\r\nerror=<@e>Đã xảy ra lỗi (<@s>{error}</>) {{report_message_l}}\\n{traceback}\r\nsee_log=<@e>Vui lòng xem tệp nhật ký để biết thêm chi tiết.</>\r\nmax=Tối đa\r\nnone=Không có\r\nunknown=Không xác định\r\n\r\nleave=\\n<@q>Cảm ơn bạn đã sử dụng Trình chỉnh sửa save file The Battle Cats!</>\r\nchecking_for_changes=<@t>Đang kiểm tra thay đổi...</>\r\nno_changes=<@su>Không tìm thấy thay đổi.</>\r\nchanges_found=<@su>Đã tìm thấy thay đổi.</>\r\n\r\ny/n=y/n\r\n\r\ngit_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.</>\r\nfailed_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</>\r\nfailed_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.</>\r\ncancel=Hủy\r\n\r\nupdate_external=Cập nhật nội dung bên ngoài\r\nupdating_external_content=<@q>Đang cập nhật nội dung bên ngoài...</>\r\n\r\ndownloading_map_names=<@q>Đang lấy tên bản đồ... (mã: <@t>{code}</>). Có thể mất một lúc...</>\r\n\r\nselect_device=Chọn thiết bị:\r\n\r\ncontinue_q=Tiếp tục? ({{y/n}}):\r\n\r\nno_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.</>\r\n\r\nfailed_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.</>\r\n\r\nno_feature_with_name=<@e>Không tìm thấy tính năng với tên: <@s>{name}</></>\r\nyes=Có\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/save.properties",
    "content": "# filename=\"save.properties\"\r\nsave_load_option=Chọn một tùy chọn để tải save file\r\ndownload_save=Tải xuống save file sử dụng Transfer Code và Confirmation Code\r\nselect_save_file=Chọn save file từ tệp\r\nadb_pull_save=Kéo save file từ thiết bị sử dụng adb\r\nwaydroid_pull_save=Kéo save file từ thiết bị waydroid\r\nload_save_data_json=Tải dữ liệu lưu từ json\r\nroot_storage_pull_save=Kéo save file từ lưu trữ root\r\nsave_save_dialog=Lưu save file\r\nsave_downloaded=<@su>Save file đã tải xuống đến <@s>{path}</>\r\nsave_json_dialog=Lưu dữ liệu lưu vào json\r\nload_from_documents=Tải save file từ thư mục tài liệu\r\nsave_file_not_found=<@e>Save file không tìm thấy</>\r\nsave_file_found=<@su>Đang tải save từ: <@t>{path}<@t></>\r\n\r\nparse_save_error=<@e>Đã xảy ra lỗi khi phân tích save file của bạn: {error}\\n{{report_message}}</>\r\nload_json_fail=<@e>Không thể tải dữ liệu lưu từ json ({error})</>\r\neditor_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}</></>\r\nsave_management=Quản lý save file\r\nsave_save=Lưu save file\r\nsave_save_file=Lưu save file vào tệp cụ thể\r\nsave_save_documents=Lưu save file vào thư mục tài liệu\r\nsave_upload=Tải lên save file đến máy chủ và lấy Transfer Code và Confirmation Code\r\nunban_account=Gỡ cấm tài khoản / Sửa lỗi save file được sử dụng ở nơi khác\r\n\r\nadb_push_rerun=Sử dụng adb để đẩy save file đến thiết bị (Chạy lại trò chơi sau khi đẩy)\r\nadb_push=Sử dụng adb để đẩy save file đến thiết bị (Không chạy lại trò chơi sau khi đẩy)\r\nadb_push_success=<@su>Save file đã đẩy đến thiết bị</>\r\nadb_push_fail=<@e>Không thể đẩy save file đến thiết bị</> ({error})\r\nadb_rerun_success=<@su>Đã chạy lại trò chơi thành công</>\r\nadb_rerun_fail=<@e>Không thể chạy lại trò chơi</> ({error})\r\n\r\nwaydroid_push_rerun=Đẩy save file đến thiết bị waydroid (Cũng chạy lại trò chơi sau khi đẩy)\r\nwaydroid_push=Đẩy save file đến thiết bị waydroid (Không chạy lại trò chơi sau khi đẩy)\r\nwaydroid_push_success=<@su>Save file đã đẩy đến thiết bị waydroid</>\r\nwaydroid_push_fail=<@e>Không thể đẩy save file đến thiết bị waydroid</> ({error})\r\nwaydroid_rerun_success=<@su>Đã chạy lại trò chơi trên thiết bị waydroid thành công</>\r\nwaydroid_rerun_fail=<@e>Không thể chạy lại trò chơi trên thiết bị waydroid</> ({error})\r\n\r\nexport_save=Xuất save file sang json\r\nsave_success=<@su>Save file đã lưu đến <@s>{path}</>\r\nexport_success=<@su>Dữ liệu lưu đã xuất đến <@s>{path}</>\r\ninit_save=Đặt lại save file\r\ninit_save_confirm=Bạn có chắc chắn muốn đặt lại save file không? ({{y/n}}):\r\ninit_save_success=<@su>Đã đặt lại save file thành công</>\r\n\r\nadb_pulling=<@q>Đang kéo save file từ thiết bị với tên gói <@s>{package_name}</>bằng adb ...</>\r\nadb_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\r\n\r\nwaydroid_pulling=<@q>Đang kéo save file từ thiết bị với tên gói <@s>{package_name}</> bằng waydroid ...</>\r\nwaydroid_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\r\n\r\nstorage_pulling=<@q>Đang kéo save file từ lưu trữ root với tên gói <@s>{package_name}</>...</>\r\nstorage_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})\r\n\r\nnot_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</>\r\n\r\nupload_items=Tải lên các vật phẩm được quản lý đến máy chủ\r\nupload_items_success=<@su>Đã tải lên các vật phẩm được quản lý thành công</>\r\nupload_items_fail=<@e>Không thể tải lên các vật phẩm được quản lý</>\r\n\r\nload_save=Tải save file\r\nload_save_success=<@su>Đã tải save file thành công</>\r\naccount=Tài khoản\r\n\r\nsave_before_exit=<@q>Lưu thay đổi mới nhất trước khi thoát? (<@s>y</>/<@s>n</>):</>\r\nsave_temp_success=<@su>Đã khôi phục save file từ tệp tạm thành công</>\r\nsave_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}\r\nsave_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)\r\n\r\ncant_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</>\r\nfailed_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\r\nfailed_to_load_save=Không thể tải save file\r\nfailed_to_save_save=Không thể lưu save file\r\n\r\ngame_version_dialog=Nhập phiên bản trò chơi (ví dụ <@t>12.2.1</>):\r\ninvalid_game_version=<@e>Phiên bản trò chơi không hợp lệ</>\r\ncountry_code_set=<@su>Đã đặt Country Code thành công thành <@s>{cc}</>\r\ngame_version_set=<@su>Đã đặt phiên bản trò chơi thành công thành <@s>{version}</>\r\n\r\nconvert_region=Chuyển đổi Country Code (ví dụ en -\\> jp)\r\nconvert_version=Chuyển đổi phiên bản trò chơi (ví dụ 12.2.1 -\\> 12.2.0)\r\n\r\ncc_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}</>\r\ngv_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}</>\r\n\r\ncreate_new_save_success=<@su>Đã tạo save file mới thành công</>\r\ncreate_new_save=Tạo save file mới\r\ncreate_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.</>\r\n\r\nparse_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 đề!</> \r\n\r\nselect_package_name=Chọn tên gói:\r\n\r\nadb_not_installed=\r\n><@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\r\n>Giá trị hiện tại: <@s>{path}</>\r\n>Lỗi: <@s>{error}</></>\r\n\r\nwaydroid_not_installed=<@e>Waydroid chưa được cài đặt, hoặc đã xảy ra lỗi: {error}</>\r\nroot_push_success=<@su>Đã ghi save vào root storage thành công</>\r\nroot_push_not_android_error=<@e>Root push chỉ khả dụng trên thiết bị Android</>\r\nroot_rerun_fail=<@e>Thất bại khi chạy lại game. Lỗi: <@s>{error}</></>\r\nroot_push=Sử dụng root để ghi save trực tiếp vào game\r\nroot_rerun_success=<@su>Đã chạy lại game thành công</>\r\nroot_push_rerun=Sử dụng root để ghi save trực tiếp vào game (và chạy lại game)\r\nroot_push_fail=<@e>Thất bại khi ghi save vào root storage. Lỗi: <@s>{error}</></>\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/server.properties",
    "content": "# filename=\"server.properties\"\r\ntransfer_code=Transfer Code\r\nenter_transfer_code=Nhập Transfer Code:\r\nconfirmation_code=Confirmation Code\r\nenter_confirmation_code=Nhập Confirmation Code:\r\ncountry_code=Country Code\r\ncountry_code_select=Chọn Country Code:\r\ninvalid_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.</>\r\ndisplay_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}}):\r\nresponse_text_display=\r\n>URL: <@q>{url}</>\r\n>Tiêu đề Yêu cầu: <@q>{request_headers}</>\r\n>Thân Yêu cầu: <@q>{request_body}</>\r\n>\r\n>Tiêu đề Phản hồi: <@q>{response_headers}</>\r\n>Thân Phản hồi: <@q>{response_body}</>\r\n\r\ndownloading_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}</>)...\r\nupload_result=\r\n><@su>\r\n>Transfer Code: <@s>{transfer_code}</>\r\n>Confirmation Code: <@s>{confirmation_code}</>\r\n></>\r\n\r\nupload_fail=<@e>Không thể tải lên save file. {{try_again_message}} {{see_log}}</>\r\nunban_fail=<@e>Không thể gỡ cấm tài khoản. {{try_again_message}} {{see_log}}</>\r\nunban_success=<@su>Tài khoản đã gỡ cấm thành công.</>\r\nupload_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}}):\r\nstrict_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ý.</>\r\ncreate_new_account_success=<@su>Tài khoản đã tạo thành công.</>\r\ncreate_new_account_fail=<@e>Không thể tạo tài khoản. {{try_again_message}} {{see_log}}</>\r\n\r\nuploading_save_file=<@q>Đang tải lên save file đến máy chủ...</>\r\ngetting_codes=<@q>Đang lấy Transfer Code và Confirmation Code...</>\r\ngetting_auth_token=<@q>Đang lấy mã xác thực tài khoản...</>\r\nrefreshing_password=<@q>Đang làm mới mật khẩu tài khoản...</>\r\ngetting_password=<@q>Đang lấy mật khẩu tài khoản...</>\r\ngetting_save_key=<@q>Đang lấy khóa lưu tài khoản...</>\r\n\r\ninquiry_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}}\r\npassword_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}}\r\n\r\nno_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.</>\r\n\r\ntransfer_backup=<@su>Đã lưu save file chuyển giao sao lưu đến <@t>{path}</></>\r\ntransfer_backup_fail=<@e>Không thể lưu save file chuyển giao sao lưu đến <@t>{path}</> do {error}</>\r\n\r\nretry_auth_token=<@e>Không thể lấy mã xác thực, đang thử lại...</>\r\ndownloading_compressed_data=<@su>Đang tải dữ liệu game nén từ <@s>{url}</></>\r\nclear_game_data_q=Bạn có muốn xóa tất cả dữ liệu game đã tải xuống? ({{y/n}}):\r\ncleared_game_data=<@su>Đã xóa dữ liệu game thành công</>\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/theme.properties",
    "content": "# filename=\"theme.properties\"\r\ntheme_text=\r\n>Chủ đề hiện tại: <@s>{theme_name}</> (Phiên bản <@s>{theme_version}</>)\r\n>Được tạo bởi <@s>{theme_author}</>\r\n>Vị trí tệp chủ đề: <@s>{theme_path}</>\r\n\r\ndefault_theme_text=\r\n>Chủ đề hiện tại: <@s>Mặc định</>\r\n>Vị trí tệp chủ đề: <@s>{theme_path}</>\r\n\r\nchecking_for_theme_updates=Đang kiểm tra cập nhật cho chủ đề bên ngoài <@t>{theme_name}</>...\r\nexternal_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}}</>\r\nexternal_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></>\r\ntheme_changed=<@su>Đã thay đổi chủ đề thành công thành <@t>{theme_name}</>.\\n{{restart_to_see_changes}}</>\r\ntheme_removed=<@su>Đã xóa chủ đề thành công <@t>{theme_name}</>.\\n{{restart_to_see_changes}}</>\r\nno_external_themes=<@e>Không tìm thấy chủ đề bên ngoài</>\r\n\r\n\r\ntheme_dialog=Chọn một chủ đề:\r\nadd_theme=Thêm chủ đề\r\nremove_theme=Xóa chủ đề\r\ntheme_remove_dialog=Chọn các chủ đề để xóa:\r\nenter_theme_git_repo=Nhập kho git của chủ đề (ví dụ <@t>https://codeberg.org/fieryhenry/ExampleEditorTheme.git</>):\r\ntheme_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}}):\r\ntheme_added=<@su>Đã thêm chủ đề thành công</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/core/updater.properties",
    "content": "# filename=\"updater.properties\"\r\nlocal_version=<@q>Phiên bản cục bộ: <@s>{local_version}</></>\r\nlatest_version=<@q>Phiên bản mới nhất: <@s>{latest_version}</></>\r\n\r\nupdate_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.</>\r\n\r\nupdate_available=\r\n><@q>Có cập nhật khả dụng: <@s>{latest_version}</>\r\n>Bạn có muốn cập nhật không? <@t>({{y/n}})</>:\r\nupdate_success=\r\n><@t>Cập nhật thành công\r\n>Vui lòng khởi động lại ứng dụng</>\r\nupdate_fail=\r\n><@e>Cập nhật thất bại\r\n>Vui lòng cập nhật thủ công</>\r\n>Lệnh: <@s>pip install --upgrade bcsfe</>\r\n\r\nversion_line={{local_version}} | {{latest_version}}\r\n\r\ndisable_update_message=Bạn có muốn tắt thông báo cập nhật không? <@t>({{y/n}}):</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/bannable_items.properties",
    "content": "# filename=\"bannable_items.properties\"\r\ndo_you_want_to_continue=Bạn có muốn tiếp tục không? ({{y/n}}):\r\n\r\ncatfood_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}}\r\nlegend_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}}\r\nrare_ticket_warning=\r\n><@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.</>\r\n>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.\r\nplatinum_ticket_warning=\r\n><@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.</>\r\n>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.\r\n\r\nselect_an_option_to_continue=Chọn tùy chọn để tiếp tục chỉnh sửa {feature_name}:\r\n\r\ncontinue_editing=Tiếp tục chỉnh sửa {feature_name}\r\ngo_to_safe_feature=Chuyển đến tính năng an toàn hơn {safer_feature_name}\r\ncancel_editing=Hủy chỉnh sửa {feature_name}\r\n\r\nrare_ticket_trade_enter=Nhập số Rare Tickets bạn muốn <@q>add</> (max value: <@q>{max}</>) (current amount: <@q>{current}</>):\r\nrare_ticket_trade_storage_full=<@e>LỖI: Bạn không có đủ chỗ trong cat storage, vui lòng giải phóng nó!</>\r\nrare_ticket_successfully_traded=\r\n><@su>Đã trao {rare_ticket_count} Rare Tickets thành công.</>\r\n>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.\r\n\r\nrare_tickets_l=Rare Tickets\r\nrare_ticket_trade_l=Rare Ticket Trade\r\n\r\nrare_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!</>\r\n\r\nplatinum_tickets_l=Platinum Tickets\r\nplatinum_shards_l=Platinum Shards"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/cats.properties",
    "content": "# filename=\"cats.properties\"\r\ntotal_selected_cats=<@t>{total}</> cats hiện đang được chọn\r\nselected_cat=<@t>{name}</> (<@t>{id}</>) đã được chọn\r\nselect_cats_rarity=Chọn cats dựa trên rarity\r\nselect_cats_name=Chọn cats dựa trên name\r\nselect_cats_obtainable=Chọn tất cả các cats có thể nhận được\r\nselect_cats_not_obtainable=Chọn tất cả các cats không thể nhận được\r\nselect_cats_gatya_banner=Chọn cats dựa trên gacha banner\r\nselect_cats_all=Chọn tất cả cats\r\nselect_cats=Chọn cats:\r\nand_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</>)?:\r\nselect_rarity=Chọn cat rarity:\r\nenter_name=Nhập cat name:\r\nselect_name=Chọn cat name:\r\nselect_gatya_banner=Nhập gacha banner ids {{range_input}}\r\ncats=Cats\r\nedit_cats=Chỉnh sửa cats\r\nenter_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}}\r\nselect_cats_id=Chọn cats theo id\r\nno_cats_found_name=<@w>Không tìm thấy cats nào với name <@s>{name}</></>\r\n\r\nselect_cats_again=Chọn thêm cats\r\nunlock_cats=Unlock Cats|Get Cats\r\nremove_cats=Remove Cats\r\nupgrade_cats=Upgrade Cats\r\ntrue_form_cats=True Form Cats\r\nremove_true_form_cats=Remove Cat True Forms\r\nupgrade_talents_cats=Upgrade Cat Talents\r\nremove_talents_cats=Remove Cat Talents\r\nunlock_cat_guide=Claim Cat Guide\r\nremove_cat_guide=Unclaim Cat Guide\r\nfinish_edit_cats=Kết thúc chỉnh sửa cats\r\nselect_edit_cats_option=Chọn tùy chọn để chỉnh sửa cats:\r\n\r\nupgrade_success=<@su>Đã upgrade cats thành công</>\r\nupgrade_cats_select_mod=Chọn tùy chọn để upgrade cats:\r\nupgrade_individual=Nhập upgrade cho từng cat đã chọn\r\nselected_cat_upgrades={{selected_cat}}: <@t>{base_level}<@s>+</>{plus_level}</>\r\nselected_cat_upgraded=<@t>{name}</> (<@t>{id}</>) đã được upgrade lên <@t>{base_level}<@s>+</>{plus_level}\r\nupgrade_all=Nhập upgrade để áp dụng cho tất cả cats đã chọn\r\nupgrade_input=\r\n>Nhập upgrade level. Ví dụ:\r\n><@t>10<@s>+</>20</> = Base level 10, plus level 20\r\n><@t>10<@s>+</></> = Base level 10, giữ current plus level\r\n><@t><@s>+</>20</> = Giữ current base level, plus level 20\r\n><@t>10</> = Base level 10, plus level 0\r\n><@t>5<@q>-</>10<@s>+</>20<@q>-</>30</> = Random base level giữa 5 và 10, random plus level giữa 20 và 30\r\n><@t>5<@q>-</>10<@s>+</></> = Random base level giữa 5 và 10, giữ current plus level\r\n><@t><@s>+</>20<@q>-</>30</> = Giữ current base level, random plus level giữa 20 và 30\r\n><@t>5<@q>-</>10</> = Random base level giữa 5 và 10, plus level 0\r\n>Nhập:\r\n\r\ntalents_success=<@su>Đã upgrade talents thành công</>\r\ntalents_individual=Nhập talents cho từng cat đã chọn\r\ntalents_all=Nhập talents để áp dụng cho tất cả cats đã chọn\r\n\r\nunlock_success=<@su>Đã unlock cats thành công</>\r\nremove_success=<@su>Đã remove cats thành công</>\r\ntrue_form_success=<@su>Đã true form cats thành công</>\r\nremove_true_form_success=<@su>Đã remove true forms thành công</>\r\nunlock_cat_guide_success=<@su>Đã claim cat guide entries thành công</>\r\nremove_cat_guide_success=<@su>Đã unclaim cat guide entries thành công</>\r\n\r\nforce_true_form_cats=Dùng trước True Form Cats\r\nforce_true_form_cats_warning=\r\n><@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.\r\n>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.</>\r\n\r\nfilter_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</>)?:\r\nselect_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)\r\nselect_cats_all_option=Chọn từ tất cả cats\r\n\r\nunlock_remove_cats=Unlock Cats / Remove Cats\r\ntrue_form_remove_form_cats=True Form Cats / Remove Cat True Forms\r\nupgrade_talents_remove_talents_cats=Upgrade Talents / Remove Talents Cats\r\nunlock_remove_cat_guide=Claim / Unclaim Cat Guide Entries\r\n\r\nunlock_remove_q=Bạn muốn <@t>Unlock</> hay <@t>Remove</> cats?:\r\ntrue_form_remove_form_q=Bạn muốn <@t>True Form</> cats hay <@t>Remove Cat True Forms</>?:\r\nupgrade_talents_remove_talents_q=Bạn muốn <@t>Upgrade</> hay <@t>Remove</> cat talents?:\r\nunlock_cat_guide_remove_guide_q=Bạn muốn <@t>Claim</> hay <@t>Unclaim</> cat guide entries?:\r\n\r\nfourth_form_remove_form_cats=Ultra Form Cats / Remove Cat Ultra Forms (4th Forms)\r\nforce_fourth_form_cats=Dùng trước Ultra Form Cats (4th Forms)\r\nfourth_form_success=<@su>Đã ultra form cats thành công</>\r\nremove_fourth_form_success=<@su>Đã remove ultra forms thành công</>\r\nfourth_form_cats=Ultra Form Cats\r\nremove_fourth_form_cats=Remove Cat Ultra Forms\r\nfourth_form_remove_form_q=Bạn muốn <@t>Ultra Form</> cats hay <@t>Remove Cat Ultra Forms</>?:\r\nforce_fourth_form_cats_warning=\r\n><@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.\r\n>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.</>\r\n\r\ngatya_info_progress=Đang tải gacha info (<@t>{current}</>/<@t>{total}</>)\r\nunknown_banner=Unknown banner\r\nbanner_txt={name} (<@s>{int}</>)\r\nfilter_down_q_gatya=Bạn muốn remove duplicate và unknown banners khỏi list? ({{y/n}}):\r\n\r\nselect_cats_non_gatya=Select Non-Gacha Cats\r\nfinished_cats_selection=Bạn đã hoàn thành việc chọn cats chưa? ({{y/n}}):\r\n\r\ninvalid_upgrade_plus=<@e>Cộng level không hợp lệ: <@s>{plus}</>\r\n\r\ntalents=Talents\r\nno_talent_data=<@w>Không có dữ liệu talent cho cat này</>\r\n\r\nselect_cats_not_unlocked=Chọn các cat chưa được mở khóa\r\n\r\ntalents_remove_success=<@su>Đã xóa talent của cat thành công</>\r\n\r\nupgrade_talents_select_mod=Chọn tùy chọn để chỉnh sửa talent cat:\r\n\r\nselect_cats_current=Chọn các cat đã mở khóa hiện tại\r\n\r\ninvalid_upgrade_base_random=<@e>Phạm vi level cơ bản không hợp lệ: <@s>{min}</>-<@s>{max}</>\r\n\r\nmax_upgrade=Cấp Nâng Tối Đa: <@t>{max_base}<@s>+</>{max_plus}</>\r\n\r\nupgrade_talent_cats=Nâng cấp talent cat\r\n\r\ninvalid_upgrade_plus_random=<@e>Phạm vi cấp cộng không hợp lệ: <@s>{min}</>-<@s>{max}</>\r\n\r\ninvalid_upgrade_base=<@e>Cấp cơ bản không hợp lệ: <@s>{base}</>\r\n\r\ntalents_version_warning=\r\n><@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.\r\n>Phiên bản Lưu: <@s>{save_version}</>\r\n>Phiên bản Dữ liệu Game: <@s>{data_version}</>\r\n>Nếu dữ liệu game đã lỗi thời, nó sẽ được cập nhật trong vài ngày tới.</>\r\n\r\ndownloading_cat_names=<@su>Đang tải tên cat từ <@s>{url}</></>\r\n\r\nitem=<@t>{name}</> (<@t>{id}</>)\r\nneed_x_more_space=<@e>Không đủ dung lượng storage. Cần thêm <@s>{needs}</> slots</>\r\nclear_storage=Clear storage\r\nselect_cats_game_version=Chọn cats theo phiên bản game\r\nunrecognised_storage_item=<@e>Không nhận diện được storage item. Danh mục item: <@s>{item_type}</>. Item id: <@s>{id}</> </>\r\nspecial_skill=<@t>{name}</> (<@t>{id}</>)\r\nselect_gv=\r\n>Nhập phiên bản game để lọc theo. Ví dụ:\r\n>- Lấy cats chỉ trong phiên bản <@t>11.5.0</>: <@t>11.5.0</>,\r\n>- 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</>\r\n>- 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</>\r\n>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</>.\r\n>Nhập:\r\n\r\ndisplay_storage=Hiển thị storage\r\nremoved_items=Các items đã xóa:\r\nremove_items=Xóa cats / skills\r\nadded_special_skills=Các special skills đã thêm:\r\nadd_special_skills=Thêm special skills / base upgrades\r\ncurrent_storage_items=Các storage items hiện tại:\r\nadded_cats=Các cats đã thêm:\r\nno_valid_gvs_entered=<@w>Không nhập phiên bản game hợp lệ nào</>\r\nadd_cats=Thêm cats\r\ntoo_many_skills_selected=<@e>Quá nhiều skills được chọn. Tối đa là <@s>{max}</>. Có <@s>{current}</></>\r\nselect_special_skills=Chọn special skills\r\nstorage_is_empty=Storage trống\r\navailable_storage=Dung lượng storage khả dụng: <@t>{slots}</>\r\nstorage_success=<@su>Đã chỉnh sửa Cat Storage thành công</>\r\ntoo_many_cats_selected=<@e>Quá nhiều cats được chọn. Tối đa là <@s>{max}</>. Có <@s>{current}</></>\r\npossible_gvs=Các phiên bản game có thể:\r\ncat_storage=Cat Storage\r\ncat=<@t>{name}</> (<@t>{id}</>)\r\n\r\n\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/enemy.properties",
    "content": "# filename=\"enemy.properties\"\r\ntotal_selected_enemies=<@t>{total}</> enemies hiện đang được chọn\r\nunlock_enemy_guide_success=<@su>Đã unlock enemy guide entries thành công</>\r\nremove_enemy_guide_success=<@su>Đã remove enemy guide entries thành công</>\r\nselected_enemy=<@t>{name}</> (<@t>{id}</>) đã được chọn\r\nselect_enemies_valid=Chọn tất cả enemies trong enemy guide\r\nselect_enemies_invalid=Chọn tất cả enemies không trong enemy guide\r\nselect_enemies_all=Chọn tất cả enemies\r\nselect_enemies_id=Chọn enemies theo ID\r\nselect_enemies_name=Chọn enemies theo name\r\nselect_enemies=Chọn enemies:\r\nenter_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}}:\r\nenter_enemy_name=Nhập enemy name:\r\nenemy_not_found_name=<@w>Không tìm thấy enemies nào với name <@s>{name}</></>\r\nunlock_enemy_guide=Mở khóa enemy guide entries\r\nremove_enemy_guide=Xóa enemy guide entries\r\nenemy_guide=Enemy Guide\r\nedit_enemy_guide=Nhập tùy chọn để chỉnh sửa enemy guide entries:"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/fixes.properties",
    "content": "# filename=\"fixes.properties\"\r\nfix_gamatoto_crash=Fix gamatoto gây crashing game\r\nfix_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)\r\n\r\nfix_ototo_crash=Fix ototo gây crashing game\r\n\r\nfix_gamatoto_crash_success=<@su>Đã fix gamatoto không gây crash game thành công</>\r\nfix_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)</></>\r\nfix_ototo_crash_success=<@su>Đã fix ototo không gây crash game thành công</>\r\n\r\nfixes=Fixes\r\n\r\nunlock_equip_menu=Mở khóa Equip Menu\r\nequip_menu_unlocked=<@su>Đã mở khóa equip menu thành công</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/gambling.properties",
    "content": "reset_wildcat_slots=<@su>Đã reset Wildcat Slots thành công</>\nreset_gambling_events=Reset Wildcat Slots và Cat Scratcher Lottery\nreset_cat_scratcher=<@su>Đã reset Cat Scratcher Lottery thành công</>\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/gamototo.properties",
    "content": "# filename=\"gamototo.properties\"\r\nenter_raw_gamatoto_xp=Nhập Raw Gamatoto XP\r\nenter_gamatoto_level=Nhập Gamatoto Level\r\nedit_gamatoto_level_q=Nhập tùy chọn để chỉnh sửa gamatoto level:\r\ngamatoto_xp=Gamatoto XP\r\ngamatoto_level=Gamatoto Level\r\ngamatoto_level_success=<@su>Đã đặt gamatoto level thành công thành <@s>{level}</> (XP: <@s>{xp}</>)</>\r\ngamatoto_level_current=<@t>Gamatoto level hiện tại là <@q>{level}</> (XP: <@q>{xp}</>)</>\r\ngamatoto_xp_level=Gamatoto XP / Level\r\n\r\ncurrent_gamatoto_helpers=Helpers hiện tại:\r\ngamatoto_helper=Helper: <@t>{name}</> (rarity: <@t>{rarity_name}</>)\r\n\r\nnew_gamatoto_helpers=New Helpers:\r\ngamatoto_helpers=Gamatoto Helpers\r\n\r\nototo_cat_cannon=Ototo Cat Cannon\r\n\r\ncurrent_cannon_stats=Cannon Stats hiện tại:\r\n\r\ncannon_part=<@t><@q>{name}</>{buffer}(level <@s>{level}</>)</>\r\ndevelopment={buffer}(Development: <@q>{development}</>)\r\ncannon_stats={parts}\r\n\r\nfoundation=Foundation\r\nstyle=Style\r\neffect=Effect\r\nimproved_foundation=Improved Foundation\r\nimproved_style=Improved Style\r\n\r\nunknown_stage=Unknown Stage (<@s>{stage}</>)\r\n\r\nselected_cannon=<@t>Selected cannon: <@q>{name}</></>\r\nselected_cannon_stage=<@t>Cannon: <@q>{name}</> Current Stage: <@q>{stage}</></>\r\n\r\ncannon_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?:\r\n\r\ncannon_dev_level_q=Bạn muốn chỉnh sửa development của các cannon hay levels của các cannon?:\r\ndevelopment_o=Development\r\nlevel_o=Levels\r\n\r\nselect_development=Chọn development stage:\r\nselect_cannon=Chọn Cannon\r\ncannon_level=Cannon Level\r\n\r\ncannon_success=<@su>Đã chỉnh sửa ototo cannons thành công</>\r\n\r\ncat_shrine=Cat Shrine\r\nshrine_level=Shrine Level\r\nshrine_xp=Shrine XP\r\ncurrent_shrine_xp_level=<@t>Current XP: <@q>{xp}</> (Level: <@q>{level}</>)</>\r\ncat_shrine_choice_dialog=Bạn muốn chỉnh sửa cat shrine <@t>level</> hay cat shrine <@t>XP</>?:\r\nshrine_level_dialog=Nhập cat shrine level (max: <@q>{max_level}</>):\r\nshrine_xp_dialog=Nhập cat shrine XP (max: <@q>{max_xp}</>):\r\ncat_shrine_edited=<@su>Đã chỉnh sửa cat shrine thành công</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/gatya.properties",
    "content": "# filename=\"gatya.properties\"\r\nevent_tickets=Event Tickets / Lucky Tickets\r\ndownloading_gatya_data=Đang tải dữ liệu sự kiện gacha...\r\ndownload_gatya_data_success=<@su>Đã tải dữ liệu sự kiện gacha thành công</>\r\ndownload_gatya_data_fail=<@e>Không thể tải dữ liệu sự kiện gacha. Có lẽ thử lại</>\r\nsave_gatya_error=<@e>Không thể lưu dữ liệu gatya do {error}</>\r\ngatya_by_id_q=Bạn có muốn chọn gacha banners theo <@t>ID</> hay <@t>tên</>?:\r\nby_name=Theo tên\r\nby_id=Theo ID\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/gold_pass.properties",
    "content": "# filename=\"gold_pass.properties\"\r\ngold_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):\r\ngold_pass=Gold Pass / Officer Club\r\ngold_pass_remove_success=<@su>Đã remove gold pass thành công</>\r\ngold_pass_get_success=<@su>Đã nhận gold pass thành công (id: <@t>{id}</>)</>\r\nofficer_pass_fixed=<@su>Đã fix officer club khỏi crash thành công</>\r\nfix_officer_pass_crash=Fix Officer Club Crashing"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/items.properties",
    "content": "# filename=\"items.properties\"\r\n# Lưu ý rằng không phải tất cả các vật phẩm đều có ở đây\r\n\r\ncatamins=Catamins\r\ncatfruit=Catfruit\r\nbase_materials=Base Materials\r\ninquiry_code=Inquiry Code\r\nrare_gatya_seed=Rare Gacha Seed\r\nnormal_gatya_seed=Normal Gacha Seed\r\nevent_gatya_seed=Event Gacha Seed\r\nunlocked_slots=Unlocked Slots|Equip Slots|Lineups\r\npassword_refresh_token=Password Refresh Token\r\nchallenge_score=Điểm Challenge\r\ndojo_score=Điểm Dojo\r\nitems=Items\r\nuser_rank_rewards=Claim phần thưởng User Rank (Không gửi phần thưởng)\r\n\r\ncatfood=CatFood\r\nxp=XP\r\nnormal_tickets=Normal Tickets|Basic Tickets|Silver Tickets\r\nrare_tickets=Rare Tickets|Gold Tickets\r\nplatinum_tickets=Platinum Tickets\r\nlegend_tickets=Legend Tickets\r\n100_million_tickets=100 Million Downloads Tickets|One Hundred Million Downloads Tickets\r\n100_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</>\r\nplatinum_shards=Platinum Shards\r\nnp=NP\r\nleadership=Leadership\r\ncatseyes=Catseyes\r\nbattle_items=Battle Items\r\ntalent_orbs=Talent Orbs\r\nscheme_items=Scheme Items\r\nlabyrinth_medals=Labyrinth Medals\r\nrestart_pack=Restart Pack|Returner Mode\r\nengineers=Engineers\r\ngamototo=Gamatoto / Ototo\r\nspecial_skills=Special Skills / Base Abilities\r\ntreasure_chests=Treasure Chests\r\nunknown_treasure_chest_name=Unknown Treasure Chest ({id})\r\n\r\nrare_ticket_trade=Rare Ticket Trade\r\nrare_ticket_trade_feature_name=Rare Ticket Trade (Cho phép lấy vé không bị ban)\r\n\r\nother=Other\r\ngatya=Gacha\r\nlevels=Levels / Story / Treasure\r\ncats_special_skills=Cats / Special Skills\r\n\r\ngatya_item_unknown_name=Unknown Item\r\nunknown_catamin_name=Unknown Catamin <@t>{id}</>\r\nunknown_catseye_name=Unknown Catseye <@t>{id}</>\r\nunknown_catfruit_name=Unknown Catfruit <@t>{id}</>\r\nunknown_labyrinth_medal_name=Unknown Labyrinth Medal <@t>{id}</>\r\n\r\nreset_golden_cat_cpus_success=<@su>Đã reset số lần sử dụng Golden Cat CPU thành công</>\r\nreset_golden_cat_cpus=Reset số lần sử dụng Golden Cat CPU\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/map.properties",
    "content": "# filename=\"map.properties\"\r\ntutorial_already_cleared=<@w>Bạn đã clear tutorial</>\r\ntutorial_cleared=<@su>Đã clear tutorial thành công</>\r\nclear_tutorial=Clear Tutorial\r\n\r\nclear_stages=Clear Stages\r\nunclear_stages=Unclear Stages\r\nclear_unclear_q=Bạn muốn <@t>clear</> hay <@t>unclear</> stages?:\r\n\r\ncurrent_enigma_stages=Current Enigma Stages:\r\nenigma_stage=Enigma Stage <@q>{name}</> (id: <@q>{id}</>) \r\nunknown_enigma_name=Unknown Enigma Name (id: <@q>{id}</>)\r\nenigma_select=Chọn Enigma Stages để Add\r\nenigma_success=<@su>Đã add Enigma Stages thành công</>\r\nwipe_enigma=Bạn muốn wipe current enigma stages của bạn? ({{y/n}}):\r\naku_realm_unlocked=<@su>Đã unlock Aku Realm thành công</>\r\nunlock_aku_realm=Unlock Aku Realm\r\n\r\nselect_story_chapters=Chọn Story Chapters\r\nchapter_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)\r\nedit_chapter_progress_all=Nhập progress để đặt mỗi chapter thành {{chapter_progress_txt}}:\r\nedit_chapter_progress=Nhập progress để đặt <@t>{chapter_name}</> thành {{chapter_progress_txt}}:\r\nedit_stage_clear_count=Nhập số lần clear stage:\r\nstory_cleared=<@su>Đã clear story thành công</>\r\nindividual_chapters=Individual Chapters\r\nall_chapters=All Chapters\r\nindividual_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?:\r\nindividual_clear_counts=Individual Clear Counts\r\nall_clear_counts=All Clear Counts\r\nindividual_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?:\r\nclear_story=Main Story Chapters|Clear Story\r\n\r\nclear=Clear\r\nunclear=Unclear\r\n\r\noutbreaks=Outbreaks / Zombie Stages\r\n\r\nclear_unclear_outbreaks=Bạn muốn <@t>clear</> hay <@t>unclear</> outbreaks?:\r\nclear_outbreaks_success=<@su>Đã clear outbreaks thành công</>\r\nunclear_outbreaks_success=<@su>Đã unclear outbreaks thành công</>\r\nno_valid_outbreaks=<@e>Lỗi: không tìm thấy outbreaks hợp lệ</>\r\n\r\naku_chapters=Aku Realm Chapters\r\naku_clear_success=<@su>Đã clear Aku Realm thành công</>\r\naku_current_stage=Aku Realm Stage <@q>{name}</> (id: <@q>{id}</>)\r\n\r\nselect_clear_type=Nhập tùy chọn để clear maps:\r\n\r\nclear_amount_all=Đặt cùng clear amount cho tất cả các chapters đã chọn\r\nclear_amount_stages=Đặt một clear amount khác nhau cho từng stage đã chọn\r\nselect_clear_amount_type=Nhập clear amount setting mode bạn muốn sử dụng:\r\nclear_amount_enter=Nhập clear amount:\r\ncustom_star_count_per_chapter=Nhập star/crown count (max <@q>{max}</>):\r\n\r\ncustom_star_count_per_chapter_unclear=\r\n>Nhập star/crown để remove:\r\n><@s><@t>1</> = unclear từ toàn bộ map</>\r\n><@s><@t>2</> = unclear từ 2nd, 3rd và 4th crown/star map</>\r\n><@s><@t>3</> = unclear từ 3rd và 4th crown/star map</>\r\n><@s><@t>4</> = unclear từ 4th crown/star map</>\r\n>(max <@q>{max}</>):\r\n\r\ncurrent_sol_chapter=Chapter <@q>{name}</> (id: <@q>{id}</>)\r\ncurrent_sol_star=Star/Crown: <@q>{star}</>\r\ncurrent_sol_stage=Stage <@q>{name}</> (id: <@q>{id}</>)\r\nmap_chapters_edited=<@su>Đã chỉnh sửa chapters thành công</>\r\nsol=Stories of Legend\r\nevent=Normal Event Stages\r\ncollab=Collaboration Event Stages\r\nselect_map=Chọn Map\r\nselect_map_dialog=\r\n>Chọn các maps bạn muốn chỉnh sửa\r\n>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</>)\r\n>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ó\r\n>Nhập:\r\nno_map_found=<@e>Không tìm thấy map với name <@s>{name}</></>\r\nfinished_selecting_maps=Bạn đã hoàn thành việc chọn maps chưa? ({{y/n}}):\r\ncurrent_maps=Maps hiện tại:\r\n\r\nselect_stage=Chọn Stage\r\n\r\ngauntlets=Gauntlets\r\ncollab_gauntlets=Collaboration Gauntlets\r\nuncanny=Uncanny Legends\r\nbehemoth_culling=Behemoth Culling\r\nlegend_quest=Legend Quest\r\ntowers=Towers\r\nzero_legends=Zero Legends\r\n\r\nunclear_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:\r\n\r\nselect_stage_progress=Nhập stage để clear lên đến và bao gồm:\r\n\r\nzero_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!</>\r\n\r\nstages_select=Nhập numbers {{range_input}}\r\n\r\nitf_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ẻ</>?\r\n\r\nfilibuster_reclearing=Bật lại Màn Filibuster\r\n\r\nunknown_map_name=Tên map không xác định (id: <@q>{id}</>)\r\n\r\ncurrent_stage={chapter_name} <@t>{stage_name}</>\r\n\r\nmodify_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}}):\r\n\r\nselect_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ể</>?:\r\n\r\nclear_amount_chapter=Đặt số lần hoàn thành khác nhau cho mỗi chapter đã chọn\r\n\r\nitf_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ờ?:\r\n\r\nunclear_specific_stages=Xóa trạng thái hoàn thành của Màn Cụ Thể\r\n\r\ncatamin_stages=Màn Catamin\r\n\r\nunclear_whole_chapters=Xóa trạng thái hoàn thành của Toàn bộ Chapter\r\n\r\nfilibuster_stage_reclearing_allowed=<@su>Đã bật lại màn Filibuster thành công.</>\r\n\r\nmap_name={name} <@s>(id: <@q>{id}</>)</>\r\n\r\nedit_map_chapters=Chọn Chapter\r\n\r\ncustom_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}}):\r\n\r\nitf_timed_score_dialog=Nhập điểm tính giờ:\r\n\r\nclear_whole_chapters=Hoàn thành Toàn bộ Chapter\r\n\r\nall_selected_stages=Tất cả các Màn đã chọn\r\n\r\nclear_specific_stages=Hoàn thành Màn Cụ Thể\r\n\r\nitf_timed_scores=Điểm tính giờ Into the Future\r\n\r\nitf_timed_scores_edited=<@su>Đã chỉnh sửa điểm tính giờ Into The Future thành công</>\r\n\r\nenter_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):\r\n\r\nselect_map_from_names=Chọn map\r\n\r\nchange_clear_amount_catamin=Thay đổi số lượng hoàn thành chapter\r\n\r\ncatamin_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</>?:\r\n\r\ncatamin_stage_success=<@su>Đã chỉnh sửa catamin stages thành công</>\r\n\r\nclear_unclear_stage_catamin=Hoàn thành / Xóa hoàn thành Catamin Stages\r\n\r\nenter_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):\r\n\r\ncatamin_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</>?:\r\n\r\nclear_enigma_stages=Clear Enigma Stages\r\ndojo_catclaw_championships=Clear Dojo Catclaw Championships\r\nadd_enigma_stages=Thêm Enigma Stages\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/medals.properties",
    "content": "# filename=\"medals.properties\"\r\nmedals=Meow Medals\r\nadd_medals=Thêm Medals\r\nremove_medals=Xóa Medals\r\nmedal_add_remove_dialog=Bạn muốn <@t>add medals</> hay <@t>remove medals</>?:\r\nmedal_string={medal_name}: <@q>{medal_req}</>\r\nselect_medals=Chọn medals:\r\nmedals_added=<@su>Đã thêm meow medals thành công</>\r\nmedals_removed=<@su>Đã xóa meow medals thành công</> "
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/missions.properties",
    "content": "# filename=\"missions.properties\"\r\nmissions=Catnip Challenges / Missions|Cat Missions\r\ncomplete_reward=Hoàn thành Missions và Không Nhận Phần Thưởng\r\ncomplete_claim=Hoàn thành Missions và Nhận Phần Thưởng\r\nuncomplete=Không Hoàn thành Mission\r\nselect_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ể</>?\r\nselect_missions=Chọn missions để chỉnh sửa:\r\nmissions_edited=<@su>Đã chỉnh sửa missions thành công</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/playtime.properties",
    "content": "# filename=\"playtime.properties\"\r\nplaytime_str=<@t>{hours}</> giờ, <@t>{minutes}</> phút, <@t>{seconds}</> giây (<@t>{frames} </>frame)\r\nplaytime_current=Thời gian chơi hiện tại: {{playtime_str}}\r\nplaytime_edited=Đã chỉnh sửa thời gian chơi thành công thành {{playtime_str}}\r\nplaytime_hours_prompt=Nhập số <@t>giờ</> để đặt thời gian chơi thành:\r\nplaytime_minutes_prompt=Nhập số <@t>phút</> để đặt thời gian chơi thành:\r\nplaytime_seconds_prompt=Nhập số <@t>giây</> để đặt thời gian chơi thành:\r\nplaytime=Thời gian chơi"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/scheme_items.properties",
    "content": "# filename=\"scheme_items.properties\"\r\nscheme_items_edit_success=<@su>Đã chỉnh sửa scheme items thành công</>\r\nscheme_items_select_gain=Chọn scheme items để nhận\r\nscheme_items_select_remove=Chọn scheme items để xóa\r\ngain_remove_scheme_items=Bạn muốn <@t>nhận</> hay <@t>xóa</> scheme items?:\r\ngain_scheme_items=Nhận scheme items\r\nremove_scheme_items=Xóa scheme items"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/special_skills.properties",
    "content": "# filename=\"special_skills.properties\"\r\nspecial_skills_dialog=Chọn một base ability để nâng cấp\r\nupgrade_individual_skill=Nhập nâng cấp cho từng skill đã chọn\r\nupgrade_all_skills=Nhập nâng cấp để áp dụng cho tất cả skill đã chọn\r\n\r\nupgrade_skills_select_mod=Chọn tùy chọn để nâng cấp skills:\r\n\r\nselected_skill=<@t>{name}</> đã được chọn\r\nselected_skill_upgrades={{selected_skill}}: <@t>{base_level}<@s>+</>{plus_level}</>\r\nselected_skill_upgraded=<@t>{name}</> đã được nâng cấp lên <@t>{base_level}<@s>+</>{plus_level}\r\nskills_edited=<@su>Đã chỉnh sửa special skills thành công</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/talent_orbs.properties",
    "content": "# filename=\"talent_orbs.properties\"\r\ntotal_current_orbs=Tổng Orbs Hiện Tại: <@q>{total_orbs}</>\r\ntotal_current_orb_types=Tổng Loại Orbs Hiện Tại: <@q>{total_types}</>\r\ncurrent_orbs=Orbs Hiện Tại:\r\norb_select=Chọn talent orbs để chỉnh sửa:\r\nselected_orbs=Talent Orbs Đã Chọn:\r\nedit_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</>)?:\r\nedit_orbs_all=Nhập giá trị để chỉnh sửa tất cả orbs đã chọn thành (tối đa <@t>{max}</>):\r\nfailed_to_load_orbs=Không thể tải talent orbs\r\n\r\nedit_orbs_help=\r\n>Trợ giúp:\r\n>Các grade khả dụng: {all_grades_str}\r\n>Các attribute khả dụng: {all_attributes_str}\r\n>Các effect khả dụng: {all_effects_str}\r\n><@w>Lưu ý: Không phải tất cả grade và effect đều khả dụng cho mọi attribute.</>\r\n>Ví dụ đầu vào:\r\n>    <c>aku</> - chọn <c>tất cả aku</> orbs\r\n>    <r>red</> <b>s</> - chọn <r>tất cả red</> orbs với <b>s</> grade\r\n>    <b>alien</> <r>d</> <r>0</> - chọn <b>alien</> orb với <r>d</> grade tăng <r>attack</>.\r\n>    <o>c</> <dm>1</> - chọn boost stories of legend orb với grade <o>c</>\r\n>Nếu bạn muốn chọn <@q>tất cả</> orbs thì nhập:\r\n>    <@q>*</>\r\n>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:\r\n>    <b>s</> <bl>black</> <g>4</>,<r>d</> <o>3</>,<g>floating</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/treasures.properties",
    "content": "# filename=\"treasures.properties\"\r\nwhole_chapters=Toàn bộ chapters\r\nindividual_stages=Từng stages riêng lẻ\r\ntreasure_groups=Nhóm treasures / Sets\r\ntreasure_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</>?:\r\ntreasures_edited=<@su>Đã chỉnh sửa treasures thành công</>\r\nper_chapter=Theo chapter\r\nall_selected_chapters=Tất cả chapters đã chọn\r\nno_treasure=Không có treasure\r\ncustom_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ì!</>)\r\ntreasure_level_dialog=Nhập level treasure bạn muốn đặt:\r\ncustom_treasure_level_dialog=Nhập level treasure tùy chỉnh bạn muốn đặt:\r\nselect_stage_by_id=Chọn stages theo IDs\r\nselect_stage_by_name=Chọn stages theo tên\r\nselect_stage_dialog=Bạn muốn chọn stages theo <@t>IDs</> hay <@t>tên</>?:\r\nselect_stage_id=Nhập IDs stage bạn muốn chọn {{range_input}}\r\nselect_stages_name=Chọn stages:\r\nselect_treasure_groups=Chọn treasure groups bạn muốn chỉnh sửa:\r\nstory_treasures=Story Treasures\r\ncurrent_chapter=Chapter hiện tại: <@t>{chapter_name}</>\r\ncurrent_treasure_group=Nhóm treasure hiện tại: <@t>{treasure_group_name}</>\r\ngroup_individual=Nhóm riêng lẻ\r\ngroup_all_at_once=Tất cả nhóm đã chọn\r\nselect_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?:\r\nedit_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ẻ</>?:\r\n"
  },
  {
    "path": "src/bcsfe/files/locales/vi/edits/user_rank.properties",
    "content": "# filename=\"user_rank.properties\"\r\nclaim=Nhận\r\nunclaim=Không Nhận\r\nfix_claimed=Sửa Đã Nhận\r\nclaim_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?:\r\nselect_ur=Chọn user rank rewards\r\nur_claimed_success=<@su>Đã nhận user rank rewards thành công</>\r\nur_unclaimed_success=<@su>Đã không nhận user rank rewards thành công</>\r\nur_string=Rank: <@s>{rank}</>: {description}\r\nur_fix_claimed_success=<@su>Đã sửa claimed user rank rewards thành công</>"
  },
  {
    "path": "src/bcsfe/files/locales/vi/metadata.json",
    "content": "{\r\n  \"authors\": [\"HungJoesifer\"],\r\n  \"name\": \"Tiếng Việt (Vietnamese)\"\r\n}\r\n"
  },
  {
    "path": "src/bcsfe/files/max_values.json",
    "content": "{\n  \"catfood\": 45000,\n  \"xp\": 99999999,\n  \"normal_tickets\": 2999,\n  \"100_million_tickets\": 9999,\n  \"rare_tickets\": 299,\n  \"platinum_tickets\": 9,\n  \"legend_tickets\": 4,\n  \"np\": 9999,\n  \"leadership\": 9999,\n  \"battle_items\": 9999,\n  \"catamins\": 9999,\n  \"catseyes\": 9999,\n  \"catfruit\": {\n    \"old\": 128,\n    \"new\": 998\n  },\n  \"base_materials\": 9999,\n  \"labyrinth_medals\": 9999,\n  \"talent_orbs\": 998,\n  \"treasure_level\": 9999,\n  \"stage_clear_count\": 9999,\n  \"itf_timed_score\": 9999,\n  \"event_tickets\": 9999,\n  \"treasure_chests\": 9999\n}\n"
  },
  {
    "path": "src/bcsfe/files/themes/default.json",
    "content": "{\n    \"short_name\": \"default\",\n    \"name\": \"Default\",\n    \"description\": \"Default theme of the editor\",\n    \"author\": \"fieryhenry\",\n    \"version\": \"1.0.0\",\n    \"colors\": {\n        \"primary\": \"#FFFFFF\",\n        \"secondary\": \"#FFFFFF\",\n        \"quaternary\": \"#008000\",\n        \"tertiary\": \"#00FFFF\",\n        \"error\": \"#FF0000\",\n        \"warning\": \"#FF0000\",\n        \"success\": \"#00FF00\"\n    }\n}"
  },
  {
    "path": "src/bcsfe/files/themes/discord.json",
    "content": "{\n  \"short_name\": \"Discord Theme\",\n  \"name\": \"Discord Theme\",\n  \"description\": \"Discord-inspired dark mode theme\",\n  \"author\": \"HungJoesifer\",\n  \"version\": \"1.0.0\",\n  \"colors\": {\n    \"primary\": \"#F0F8FF\",\n    \"secondary\": \"#A6B3F8\",\n    \"tertiary\": \"#5865F2\",\n    \"quaternary\": \"#FFFFFF\",\n    \"error\": \"#ED4245\",\n    \"warning\": \"#F1C40F\",\n    \"success\": \"#57F287\"\n  }\n}\n"
  },
  {
    "path": "src/bcsfe/py.typed",
    "content": ""
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_parse.py",
    "content": "from bcsfe import core\n\n\ndef run():\n    saves_path = core.Path(__file__).parent().add(\"saves\")\n\n    for file in saves_path.get_files():\n        print(f\"Testing {file.basename()}\")\n        data1 = file.read()\n\n        save_1 = core.SaveFile(data1)\n        data_2 = save_1.to_data()\n\n        assert data1 == data_2\n\n        json_data_1 = save_1.to_dict()\n\n        save_3 = core.SaveFile.from_dict(json_data_1)\n        json_data_2 = save_3.to_dict()\n\n        assert json_data_1 == json_data_2\n\n        data_3 = save_3.to_data()\n\n        assert data1 == data_3\n\n        print(f\"Tested {file.basename()} {save_1.game_version}\")\n"
  }
]