├── .github └── workflows │ ├── accum-gh-clone-stats.yml │ └── accum-gh-view-stats.yml ├── .gitignore ├── BAAuto.py ├── LICENSE ├── README.md ├── ascreencap_arm64-v8a ├── ascreencap_armeabi-v7a ├── ascreencap_local ├── ascreencap_x86 ├── ascreencap_x86_64 ├── assets ├── CN │ ├── cafe │ │ ├── available.png │ │ ├── confirm.png │ │ ├── earnings.png │ │ ├── invite.png │ │ └── momotalk.png │ ├── claim_rewards │ │ ├── claim.png │ │ ├── mailbox_claim_all.png │ │ └── tasks_claim_all.png │ ├── farming │ │ ├── big_enter.png │ │ ├── confirm.png │ │ ├── hard.png │ │ ├── normal.png │ │ ├── quest.png │ │ ├── small_enter.png │ │ ├── stars_required.png │ │ ├── sweep.png │ │ └── sweep_complete.png │ ├── goto │ │ ├── bounty.png │ │ ├── cafe.png │ │ ├── campaign.png │ │ ├── club.png │ │ ├── commissions.png │ │ ├── event.png │ │ ├── event_banner.png │ │ ├── home.png │ │ ├── mailbox.png │ │ ├── menu.png │ │ ├── mission.png │ │ ├── scrimmage.png │ │ ├── settings.png │ │ ├── skip.png │ │ ├── tactical_challenge.png │ │ └── tasks.png │ └── tactical_challenge │ │ ├── battle_result.png │ │ ├── best.png │ │ ├── claim │ │ ├── 0.png │ │ └── 1.png │ │ ├── formation.png │ │ ├── lose.png │ │ ├── mobilise.png │ │ ├── no_tick.png │ │ ├── no_tickets.png │ │ └── win.png └── EN │ ├── cafe │ ├── available.png │ ├── confirm.png │ ├── earnings.png │ ├── invite.png │ └── momotalk.png │ ├── claim_rewards │ ├── claim.png │ ├── mailbox_claim_all.png │ └── tasks_claim_all.png │ ├── farming │ ├── big_enter.png │ ├── confirm.png │ ├── hard.png │ ├── normal.png │ ├── quest.png │ ├── small_enter.png │ ├── stars_required.png │ ├── sweep.png │ └── sweep_complete.png │ ├── goto │ ├── bounty.png │ ├── cafe.png │ ├── campaign.png │ ├── club.png │ ├── commissions.png │ ├── event.png │ ├── event_banner.png │ ├── home.png │ ├── mailbox.png │ ├── menu.png │ ├── mission.png │ ├── scrimmage.png │ ├── settings.png │ ├── skip.png │ ├── tactical_challenge.png │ └── tasks.png │ └── tactical_challenge │ ├── battle_result.png │ ├── best.png │ ├── claim │ ├── 0.png │ └── 1.png │ ├── formation.png │ ├── lose.png │ ├── mobilise.png │ ├── no_tick.png │ ├── no_tickets.png │ └── win.png ├── config.json ├── gui ├── custom_widgets │ ├── __init__.py │ ├── ctk_integerspinbox.py │ ├── ctk_notification.py │ ├── ctk_scrollable_dropdown.py │ ├── ctk_scrollable_dropdown_frame.py │ ├── ctk_templatedialog.py │ ├── ctk_timeentry.py │ ├── ctk_tooltip.py │ ├── ctkmessagebox.py │ └── icons │ │ ├── cancel.png │ │ ├── check.png │ │ ├── info.png │ │ ├── question.png │ │ └── warning.png ├── frames │ ├── __init__.py │ ├── cafe.py │ ├── claim_rewards.py │ ├── farming.py │ ├── logger.py │ ├── login.py │ └── sidebar.py ├── icons │ ├── gear_off.png │ ├── gear_on.png │ ├── karin.ico │ └── karin.png └── student_list │ ├── CN.json │ ├── CNscraper.py │ └── EN.json ├── licenses ├── ALAuto-license ├── CTkMessagebox-license ├── CTkScrollableDropdown-license ├── CTkToolTip-license └── ascreencap-license ├── modules ├── __init__.py ├── bounty.py ├── cafe.py ├── claim_rewards.py ├── login.py ├── mission.py ├── scrimmage.py └── tactical_challenge.py ├── requirements.txt ├── script.py ├── traceback.log └── util ├── __init__.py ├── adb.py ├── config.py ├── config_consts.py ├── emulator.py ├── exceptions.py ├── logger.py └── utils.py /.github/workflows/accum-gh-clone-stats.yml: -------------------------------------------------------------------------------- 1 | name: GitHub clones counter for 14 days at every 8 hours and clones accumulator 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */8 * * *" 6 | # Allows you to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | accum-gh-clone-stats: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: RedDeadDepresso/gh-action--accum-gh-stats@master 15 | with: 16 | deps_repo_owner: RedDeadDepresso 17 | deps_repo_branch: master 18 | deps_repo_read_token: ${{ github.token }} 19 | 20 | stat_repo_owner: RedDeadDepresso 21 | stat_repo: BAAuto 22 | stat_entity: clones 23 | stat_repo_read_token: ${{ secrets.READ_STATS_TOKEN }} 24 | stats_list_key: clones 25 | 26 | #commit_msg_entity: cl 27 | 28 | curl_flags: >- 29 | -H 'Cache-Control: no-cache' 30 | 31 | output_repo_owner: RedDeadDepresso 32 | output_repo: BAAuto--gh-stats 33 | output_repo_branch: master 34 | output_repo_dir: traffic/clones 35 | output_repo_write_token: ${{ secrets.WRITE_STATS_TOKEN }} 36 | 37 | flags: >- 38 | ENABLE_PRINT_INITIAL_ENV_INTO_STDOUT=1 39 | 40 | env: >- 41 | ENABLE_GENERATE_CHANGELOG_FILE=1 42 | ENABLE_COMMIT_MESSAGE_DATE_WITH_TIME=1 43 | ENABLE_COMMIT_MESSAGE_DATE_TIME_WITH_LAST_CHANGED_DATE_OFFSET=1 44 | ENABLE_COMMIT_MESSAGE_WITH_WORKFLOW_RUN_NUMBER=1 45 | ENABLE_GITHUB_ACTIONS_RUN_URL_PRINT_TO_CHANGELOG=1 46 | ENABLE_REPO_STATS_COMMITS_URL_PRINT_TO_CHANGELOG=1 47 | # CONTINUE_ON_INVALID_INPUT=1 48 | # CONTINUE_ON_EMPTY_CHANGES=1 49 | # CONTINUE_ON_RESIDUAL_CHANGES=1 50 | # CHANGELOG_FILE=changelog.txt 51 | -------------------------------------------------------------------------------- /.github/workflows/accum-gh-view-stats.yml: -------------------------------------------------------------------------------- 1 | name: GitHub views counter for 14 days at every 8 hours and views accumulator 2 | 3 | on: 4 | schedule: 5 | - cron: "0 */8 * * *" 6 | # Allows you to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | accum-gh-view-stats: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: RedDeadDepresso/gh-action--accum-gh-stats@master 15 | with: 16 | deps_repo_owner: RedDeadDepresso 17 | deps_repo_branch: master 18 | deps_repo_read_token: ${{ github.token }} 19 | 20 | stat_repo_owner: RedDeadDepresso 21 | stat_repo: BAAuto 22 | stat_entity: views 23 | stat_repo_read_token: ${{ secrets.READ_STATS_TOKEN }} 24 | stats_list_key: views 25 | 26 | #commit_msg_entity: vi 27 | 28 | curl_flags: >- 29 | -H 'Cache-Control: no-cache' 30 | 31 | output_repo_owner: RedDeadDepresso 32 | output_repo: BAAuto--gh-stats 33 | output_repo_branch: master 34 | output_repo_dir: traffic/views 35 | output_repo_write_token: ${{ secrets.WRITE_STATS_TOKEN }} 36 | 37 | flags: >- 38 | ENABLE_PRINT_INITIAL_ENV_INTO_STDOUT=1 39 | 40 | env: >- 41 | ENABLE_GENERATE_CHANGELOG_FILE=1 42 | ENABLE_COMMIT_MESSAGE_DATE_WITH_TIME=1 43 | ENABLE_COMMIT_MESSAGE_DATE_TIME_WITH_LAST_CHANGED_DATE_OFFSET=1 44 | ENABLE_COMMIT_MESSAGE_WITH_WORKFLOW_RUN_NUMBER=1 45 | ENABLE_GITHUB_ACTIONS_RUN_URL_PRINT_TO_CHANGELOG=1 46 | ENABLE_REPO_STATS_COMMITS_URL_PRINT_TO_CHANGELOG=1 47 | # CONTINUE_ON_INVALID_INPUT=1 48 | # CONTINUE_ON_EMPTY_CHANGES=1 49 | # CONTINUE_ON_RESIDUAL_CHANGES=1 50 | # CHANGELOG_FILE=changelog.txt 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /BAAuto.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | import threading 4 | import customtkinter 5 | import sys 6 | import platform 7 | import customtkinter 8 | import subprocess 9 | import os 10 | 11 | from util.emulator import Emulator 12 | from gui.frames.sidebar import Sidebar 13 | from gui.frames.logger import LoggerTextBox 14 | from gui.custom_widgets.ctk_notification import CTkNotification 15 | from gui.custom_widgets.ctkmessagebox import CTkMessagebox 16 | 17 | class Config: 18 | def __init__(self, linker, config_file): 19 | self.linker = linker 20 | self.config_file = config_file 21 | self.config_data = self.read() 22 | self.linker.widgets = self.set_values_to_none(self.config_data) 23 | linker.config = self 24 | 25 | def read(self): 26 | # Read the JSON file 27 | try: 28 | with open(self.config_file, 'r') as json_file: 29 | config_data = json.load(json_file) 30 | return config_data 31 | except FileNotFoundError: 32 | print(f"Config file '{self.config_file}' not found.") 33 | sys.exit(1) 34 | except json.JSONDecodeError: 35 | print(f"Invalid JSON format in '{self.config_file}'.") 36 | sys.exit(1) 37 | 38 | def set_values_to_none(self, input_dict): 39 | result = {} 40 | for key, value in input_dict.items(): 41 | if isinstance(value, dict): 42 | result[key] = self.set_values_to_none(value) 43 | else: 44 | result[key] = None 45 | return result 46 | 47 | def load_config(self, widgets=None, config_data=None): 48 | if widgets == None: 49 | widgets = self.linker.widgets 50 | config_data = self.config_data 51 | for key in widgets: 52 | if isinstance(widgets[key], dict) and isinstance(config_data[key], dict): 53 | self.load_config(widgets[key], config_data[key]) 54 | else: 55 | if widgets[key] is not None: 56 | if isinstance(widgets[key], customtkinter.CTkCheckBox): 57 | if config_data[key] == True: 58 | widgets[key].select() 59 | else: 60 | widgets[key].deselect() 61 | elif isinstance(widgets[key], customtkinter.CTkEntry): 62 | widgets[key].insert(0, config_data[key]) 63 | else: 64 | widgets[key].set(config_data[key]) 65 | 66 | def save_to_json(self, list_keys): 67 | widget = self.linker.widgets 68 | data = self.config_data 69 | for i in list_keys[:-1]: 70 | widget = widget[i] 71 | data = data[i] 72 | widget = widget[list_keys[-1]] 73 | value = widget.get() 74 | if isinstance(widget, customtkinter.CTkCheckBox): 75 | value = True if value==1 else False 76 | data[list_keys[-1]] = value 77 | self.save_file("Configuration") 78 | 79 | def save_file(self, name=None): 80 | with open("config.json", "w") as config_file: 81 | json.dump(self.config_data, config_file, indent=2) 82 | if name: 83 | self.linker.show_notification(name) 84 | 85 | class Linker: 86 | def __init__(self): 87 | self.capitalise = lambda word: " ".join(x.title() for x in word.split("_")) 88 | self.config = None 89 | self.widgets = {} 90 | # frames 91 | self.sidebar = None 92 | self.modules_dictionary = {} 93 | self.logger = None 94 | # script.py process 95 | self.script = None 96 | self.name_to_sidebar_frame = { 97 | "Template": None, 98 | "Queue": None, 99 | "Configuration":None 100 | } 101 | self.should_then = False 102 | 103 | def terminate_script(self): 104 | # If process is running, terminate it 105 | self.script.terminate() 106 | self.script = None 107 | self.sidebar.start_button.configure(text="Start", fg_color = ['#3B8ED0', '#1F6AA5']) 108 | self.switch_queue_state("normal") 109 | 110 | def switch_student_list(self): 111 | server = self.config.config_data["login"]["server"] 112 | with open(f"gui/student_list/{server}.json", "r") as f: 113 | student_list = json.load(f) 114 | cafe_frame = self.modules_dictionary["cafe"]["frame"] 115 | cafe_frame.student_dropdown.configure(values=student_list) 116 | 117 | def start_stop(self): 118 | if hasattr(self, 'script') and self.script is not None: 119 | self.terminate_script() 120 | else: 121 | # If process is not running, start it 122 | self.script = subprocess.Popen(['python', 'script.py'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 123 | threading.Thread(target=self.read_output).start() 124 | self.sidebar.start_button.configure(text="Stop", fg_color = "crimson") 125 | self.switch_queue_state("disabled") 126 | 127 | def read_output(self): 128 | while self.script is not None: 129 | line = self.script.stdout.readline().decode('utf-8') 130 | for string in ['Terminating...', "All assigned tasks were executed."]: 131 | if string in line or line == '': 132 | if hasattr(self, 'script') and self.script is not None: 133 | self.sidebar.master.after(10, self.terminate_script) 134 | 135 | if "All assigned tasks were executed." in line: 136 | self.sidebar.master.after(10, self.execute_then) 137 | return # Break the loop if there's no more output and the subprocess has finished 138 | 139 | # Check if line contains any log level 140 | for level, color in self.logger.log_level_colors.items(): 141 | if level in line: 142 | # Display output in text box with color 143 | self.logger.log_textbox.configure(state="normal") 144 | self.logger.log_textbox.insert("end", line, level) 145 | self.logger.log_textbox.configure(state="disabled") 146 | break 147 | 148 | if self.logger.autoscroll_enabled: 149 | self.logger.log_textbox.yview_moveto(1.0) 150 | 151 | def switch_queue_state(self, state): 152 | farming_frame = self.modules_dictionary["farming"]["frame"] 153 | for button in farming_frame.queue_buttons: 154 | button.configure(state=state) 155 | self.update_queue() 156 | for frame in farming_frame.queue_frames: 157 | for widget in frame.winfo_children(): 158 | widget.configure(state=state) 159 | 160 | def update_queue(self): 161 | farming_frame = self.modules_dictionary["farming"]["frame"] 162 | farming_frame.clear_frames(queue=True) 163 | new_config_data = self.config.read() 164 | self.config.config_data["farming"]['mission']['queue'] = new_config_data["farming"]['mission']['queue'] 165 | for entry in self.config.config_data["farming"]['mission']['queue']: 166 | farming_frame.add_frame(entry, queue=True) 167 | 168 | def show_notification(self, name): 169 | sidebar_frame = self.name_to_sidebar_frame[name] 170 | if self.script: 171 | new_notification = CTkNotification(text= f"{name} was saved but will be read by the script in the next run.", master=sidebar_frame, fg_color="orange") 172 | else: 173 | new_notification = CTkNotification(text= f"{name} was saved successfully.", master=sidebar_frame, fg_color="green") 174 | new_notification.grid(row=0, column=0, sticky="nsew") 175 | self.sidebar.master.after(2500, new_notification.destroy) 176 | 177 | def execute_then(self): 178 | if hasattr(self, 'script') and self.script is not None: 179 | self.terminate_script() 180 | 181 | then = self.config.config_data["then"] 182 | emulator_path = self.config.config_data["login"]["emulator_path"] 183 | 184 | if then == "Do Nothing": 185 | return 186 | 187 | elif then == "Exit BAAuto": 188 | self.sidebar.master.destroy() 189 | 190 | elif then == "Exit Emulator": 191 | if os.path.isfile(emulator_path): 192 | Emulator.terminate(emulator_path) 193 | 194 | elif then == "Exit BAAuto and Emulator": 195 | if os.path.isfile(emulator_path): 196 | Emulator.terminate(emulator_path) 197 | self.sidebar.master.destroy() 198 | 199 | elif then == "Shutdown": 200 | subprocess.run("shutdown -s -t 60", shell=True) 201 | msg = CTkMessagebox(title="Cancel Shutdown?", message="All tasks have been completed: shutting down. Do you want to cancel?", 202 | icon="question", option_1="Cancel") 203 | response = msg.get() 204 | if response=="Cancel": 205 | subprocess.run("shutdown -a", shell=True) 206 | 207 | class App(customtkinter.CTk): 208 | def __init__(self): 209 | super().__init__("#18173C") 210 | self.configure_window() 211 | linker = Linker() 212 | config = Config(linker, "config.json") 213 | sidebar = Sidebar(self, linker, config, fg_color="#25224F") 214 | sidebar.grid(row=0, column=0, sticky="nsw") 215 | logger = LoggerTextBox(self, linker, config, fg_color="#262250") 216 | logger.grid(row=0, column=2, pady=20, sticky="nsew") 217 | config.load_config() 218 | if config.config_data["login"]["auto_start"]: 219 | self.after(10, linker.start_stop) 220 | 221 | def configure_window(self): 222 | self.title("BAAuto") 223 | self.geometry(f"{1500}x{850}") 224 | self.iconbitmap('gui/icons/karin.ico') 225 | """ 226 | solution to Settings Frame and Logger Frame widths not being 227 | consistent between different windows scaling factoro#s 228 | """ 229 | self.scaling_factor = self.get_scaling_factor() 230 | self.grid_columnconfigure(0, weight=0) 231 | self.grid_columnconfigure(1, weight=0, minsize=650*self.scaling_factor) 232 | self.grid_columnconfigure(2, weight=1, minsize=506*self.scaling_factor) 233 | self.grid_rowconfigure(0, weight=1) 234 | 235 | def get_scaling_factor(self): 236 | system = platform.system() 237 | 238 | if system == 'Windows': 239 | import ctypes 240 | user32 = ctypes.windll.user32 241 | return user32.GetDpiForSystem() / 96.0 242 | 243 | if system == 'Darwin': # macOS 244 | from Quartz import CGDisplayScreenSize, CGDisplayPixelsWide 245 | screen_size = CGDisplayScreenSize(0) 246 | screen_width = CGDisplayPixelsWide(0) 247 | return screen_width / screen_size[0] 248 | 249 | if system == 'Linux': 250 | command = ["gsettings", "get", "org.gnome.desktop.interface", "scaling-factor"] 251 | try: 252 | output = subprocess.check_output(command).decode("utf-8") 253 | return float(output.strip()) 254 | except subprocess.CalledProcessError: 255 | return 1.0 # Default to 100% if unable to retrieve scaling factor 256 | 257 | return 1.0 # Default scaling factor for unknown or unsupported systems 258 | 259 | if __name__ == "__main__": 260 | app = App() 261 | app.mainloop() 262 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RedDeadDepresso 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BAAuto: Blue Archive Automation Script 2 | ![example](https://github.com/RedDeadDepresso/BAAuto/assets/94017243/8c661360-5667-401a-986d-3fb0f7400462) 3 | 4 | BAAuto is a Python-based automation script designed to streamline various tasks in Blue Archive. It's a modified version of Egoistically's [ALAuto](https://github.com/Egoistically/ALAuto) and with a user-friendly GUI, BAAuto simplifies tasks such as login, cafe, bounty, scrimmage, tactical challenge, mission, and reward claiming. It's only for the Global server with support for English and Chinese languages but you can find other Blue Archive scripts for different servers [here](#other-blue-archive-scripts). 5 | 6 | ~~UPDATE: I'm currently adapting [ArisuAutoSweeper](https://github.com/TheFunny/ArisuAutoSweeper), a script for Blue Archive built on the Alas framework, to EN. This decision stems from the fact that BAAuto is sluggish, lacks optimisation, and has a buggy GUI. Also, some of BAAuto's features will be implemented into ArisuAutoSweeper. 7 | I will try to fix any bugs I encounter in BAAuto during this process but no new features will be added. You can therefore reuse the config.json from previous versions of BAAuto by simply copying and pasting it.~~ 8 | 9 | UPDATE: BAAuto will be no longer maintained. 10 | 11 | ## Table of Contents 12 | - [Requirements on Windows](#requirements-on-windows) 13 | - [Supported Emulators](#supported-emulators) 14 | - [Graphics Settings in Blue Archive](#graphics-settings-in-blue-archive) 15 | - [Installation and Usage](#installation-and-usage) 16 | - [Known Bugs](#known-bugs) 17 | - [Acknowledgment](#acknowledgment) 18 | - [Other Blue Archive Scripts](#other-blue-archive-scripts) 19 | 20 | ## Requirements on Windows 21 | To use BAAuto effectively, you'll need the following: 22 | 23 | - Python 3.11 or latest, installed and added to your system's PATH. 24 | - The latest [ADB](https://developer.android.com/studio/releases/platform-tools) added to your system's PATH. 25 | - An ADB-debugging enabled emulator with a resolution of **1280x720** and running Android 5 or newer. 26 | 27 | [Here are some detailed instructions to set up the environment](https://github.com/RedDeadDepresso/BAAuto/issues/7#issuecomment-1747275236) 28 | 29 | ## Supported Emulators 30 | BAAuto has been tested and confirmed to work with the following emulators: 31 | 32 | - Bluestacks 33 | - LDPlayer9 34 | - MuMu Player 35 | 36 | While other emulators may work, these have been tested extensively and are recommended for optimal performance. 37 | 38 | ## Graphics Settings in Blue Archive 39 | For the best experience, configure your Blue Archive settings as follows: 40 | 41 | - Minimum: Medium graphics settings at 30fps. 42 | - Recommended: Medium, High, or Very High graphics settings at 60fps. 43 | 44 | Please note that BAAuto may require adjustments to the source code if you choose lower graphics settings than the ones listed above. 45 | 46 | ## Installation and Usage 47 | Follow these steps to get BAAuto up and running: 48 | 49 | 1. Clone or download this repository. 50 | 2. Install the required packages using `pip3` with the command `pip3 install -r requirements.txt`. 51 | 3. Ensure that ADB debugging is enabled on your emulator. 52 | 4. Run `BAAuto.py` and modify the connection address to match your emulator's ADB port and configure other settings to your preference. 53 | 5. Changes are automatically saved except for the Mission/Commissions/Event section. 54 | 6. For Event, you need to upload a cropped image of the event banner (without a background) in the `assets/EN/goto` or `assets/CN/goto` directory and save it as `event_banner.png`. 55 | 7. Avoid setting the number of sweeps higher than 30, as this may cause BAAuto to assume the game is stuck. 56 | 8. If you've enabled "Tap Students" in the Cafe, make sure to zoom out and swipe to the bottom-left corner before starting the script. You can make a pull request if you have a solution for zooming out using ADB. 57 | 9. If you've selected "Exit Emulator" or "Exit BAAuto and Emulator", provide the emulator path. For BlueStacks instances, create a shortcut for Blue Archive from the instance and choose it as the emulator path to ensure BAAuto only closes that specific instance. Also, launch Blue Archive from the shortcut everytime you plan to close it with BAAuto. 58 | 10. If you'd like to create new assets, you can refer to [this guide](https://github.com/Egoistically/ALAuto/wiki/Creating-new-assets-for-bot). 59 | 60 | Please feel free to use and modify BAAuto as you see fit. Your feedback and contributions are always welcome. 61 | 62 | ## Known Bugs 63 | Here are some known issues with BAAuto: 64 | 65 | - Ascreencap does not work; use uiautomator2 instead. 66 | - The script relies heavily on "time.sleep()," which can make it slower at times. 67 | - There's no implementation of data validation for the "config.json" file. The GUI and script may crash if the file is corrupted or contains incorrect data. 68 | - Sometimes BAAuto thinks the script is still running when it isn't. This is due to some threading issues. 69 | 70 | ## Acknowledgment 71 | I'd like to express my gratitude to the following individuals, listed in no particular order: 72 | 73 | - [Akascape](https://github.com/Akascape): Provided customtkinter widgets that are both complex and user-friendly. 74 | - [Egoistically](https://github.com/Egoistically): For making ALAuto open-source and providing the foundation for BAAuto. 75 | - [hgjhgj](https://github.com/hgjazhgj): Created the OCR library BAAuto is using and the first to star the repository, which greatly motivated me. 76 | - [LmeSzinc](https://github.com/LmeSzinc) and [MistEO](https://github.com/MistEO): Creators of Alas and MAA, respectively, who inspired me to develop BAAuto. 77 | - [TomSchimansky](https://github.com/TomSchimansky): Created customtkinter, making it possible for an inexperienced programmer like me to create a modern GUI. 78 | 79 | ## Other Blue Archive Scripts 80 | Many people have created Blue Archive scripts for different servers. 81 | 82 | - [ArisuAutoSweeper](https://github.com/TheFunny/ArisuAutoSweeper): Blue Archive Automation Script for JP and Global EN 83 | - [baas](https://github.com/baas-pro/baas): Blue Archive Auto Script for CN 84 | - [BlueArchiveAutoScript](https://github.com/pur1fying/blue_archive_auto_script): BAAS, used to implement Blue Archive 85 | automation for CN 86 | - [MBA](https://github.com/MaaAssistantArknights/MBA): BA assistant based on the new architecture of MAA, support planned for all servers but in hiatus 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /ascreencap_arm64-v8a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/ascreencap_arm64-v8a -------------------------------------------------------------------------------- /ascreencap_armeabi-v7a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/ascreencap_armeabi-v7a -------------------------------------------------------------------------------- /ascreencap_local: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/ascreencap_local -------------------------------------------------------------------------------- /ascreencap_x86: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/ascreencap_x86 -------------------------------------------------------------------------------- /ascreencap_x86_64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/ascreencap_x86_64 -------------------------------------------------------------------------------- /assets/CN/cafe/available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/cafe/available.png -------------------------------------------------------------------------------- /assets/CN/cafe/confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/cafe/confirm.png -------------------------------------------------------------------------------- /assets/CN/cafe/earnings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/cafe/earnings.png -------------------------------------------------------------------------------- /assets/CN/cafe/invite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/cafe/invite.png -------------------------------------------------------------------------------- /assets/CN/cafe/momotalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/cafe/momotalk.png -------------------------------------------------------------------------------- /assets/CN/claim_rewards/claim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/claim_rewards/claim.png -------------------------------------------------------------------------------- /assets/CN/claim_rewards/mailbox_claim_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/claim_rewards/mailbox_claim_all.png -------------------------------------------------------------------------------- /assets/CN/claim_rewards/tasks_claim_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/claim_rewards/tasks_claim_all.png -------------------------------------------------------------------------------- /assets/CN/farming/big_enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/big_enter.png -------------------------------------------------------------------------------- /assets/CN/farming/confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/confirm.png -------------------------------------------------------------------------------- /assets/CN/farming/hard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/hard.png -------------------------------------------------------------------------------- /assets/CN/farming/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/normal.png -------------------------------------------------------------------------------- /assets/CN/farming/quest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/quest.png -------------------------------------------------------------------------------- /assets/CN/farming/small_enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/small_enter.png -------------------------------------------------------------------------------- /assets/CN/farming/stars_required.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/stars_required.png -------------------------------------------------------------------------------- /assets/CN/farming/sweep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/sweep.png -------------------------------------------------------------------------------- /assets/CN/farming/sweep_complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/farming/sweep_complete.png -------------------------------------------------------------------------------- /assets/CN/goto/bounty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/bounty.png -------------------------------------------------------------------------------- /assets/CN/goto/cafe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/cafe.png -------------------------------------------------------------------------------- /assets/CN/goto/campaign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/campaign.png -------------------------------------------------------------------------------- /assets/CN/goto/club.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/club.png -------------------------------------------------------------------------------- /assets/CN/goto/commissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/commissions.png -------------------------------------------------------------------------------- /assets/CN/goto/event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/event.png -------------------------------------------------------------------------------- /assets/CN/goto/event_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/event_banner.png -------------------------------------------------------------------------------- /assets/CN/goto/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/home.png -------------------------------------------------------------------------------- /assets/CN/goto/mailbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/mailbox.png -------------------------------------------------------------------------------- /assets/CN/goto/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/menu.png -------------------------------------------------------------------------------- /assets/CN/goto/mission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/mission.png -------------------------------------------------------------------------------- /assets/CN/goto/scrimmage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/scrimmage.png -------------------------------------------------------------------------------- /assets/CN/goto/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/settings.png -------------------------------------------------------------------------------- /assets/CN/goto/skip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/skip.png -------------------------------------------------------------------------------- /assets/CN/goto/tactical_challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/tactical_challenge.png -------------------------------------------------------------------------------- /assets/CN/goto/tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/goto/tasks.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/battle_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/battle_result.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/best.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/best.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/claim/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/claim/0.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/claim/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/claim/1.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/formation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/formation.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/lose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/lose.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/mobilise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/mobilise.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/no_tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/no_tick.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/no_tickets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/no_tickets.png -------------------------------------------------------------------------------- /assets/CN/tactical_challenge/win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/CN/tactical_challenge/win.png -------------------------------------------------------------------------------- /assets/EN/cafe/available.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/cafe/available.png -------------------------------------------------------------------------------- /assets/EN/cafe/confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/cafe/confirm.png -------------------------------------------------------------------------------- /assets/EN/cafe/earnings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/cafe/earnings.png -------------------------------------------------------------------------------- /assets/EN/cafe/invite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/cafe/invite.png -------------------------------------------------------------------------------- /assets/EN/cafe/momotalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/cafe/momotalk.png -------------------------------------------------------------------------------- /assets/EN/claim_rewards/claim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/claim_rewards/claim.png -------------------------------------------------------------------------------- /assets/EN/claim_rewards/mailbox_claim_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/claim_rewards/mailbox_claim_all.png -------------------------------------------------------------------------------- /assets/EN/claim_rewards/tasks_claim_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/claim_rewards/tasks_claim_all.png -------------------------------------------------------------------------------- /assets/EN/farming/big_enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/big_enter.png -------------------------------------------------------------------------------- /assets/EN/farming/confirm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/confirm.png -------------------------------------------------------------------------------- /assets/EN/farming/hard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/hard.png -------------------------------------------------------------------------------- /assets/EN/farming/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/normal.png -------------------------------------------------------------------------------- /assets/EN/farming/quest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/quest.png -------------------------------------------------------------------------------- /assets/EN/farming/small_enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/small_enter.png -------------------------------------------------------------------------------- /assets/EN/farming/stars_required.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/stars_required.png -------------------------------------------------------------------------------- /assets/EN/farming/sweep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/sweep.png -------------------------------------------------------------------------------- /assets/EN/farming/sweep_complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/farming/sweep_complete.png -------------------------------------------------------------------------------- /assets/EN/goto/bounty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/bounty.png -------------------------------------------------------------------------------- /assets/EN/goto/cafe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/cafe.png -------------------------------------------------------------------------------- /assets/EN/goto/campaign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/campaign.png -------------------------------------------------------------------------------- /assets/EN/goto/club.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/club.png -------------------------------------------------------------------------------- /assets/EN/goto/commissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/commissions.png -------------------------------------------------------------------------------- /assets/EN/goto/event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/event.png -------------------------------------------------------------------------------- /assets/EN/goto/event_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/event_banner.png -------------------------------------------------------------------------------- /assets/EN/goto/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/home.png -------------------------------------------------------------------------------- /assets/EN/goto/mailbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/mailbox.png -------------------------------------------------------------------------------- /assets/EN/goto/menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/menu.png -------------------------------------------------------------------------------- /assets/EN/goto/mission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/mission.png -------------------------------------------------------------------------------- /assets/EN/goto/scrimmage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/scrimmage.png -------------------------------------------------------------------------------- /assets/EN/goto/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/settings.png -------------------------------------------------------------------------------- /assets/EN/goto/skip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/skip.png -------------------------------------------------------------------------------- /assets/EN/goto/tactical_challenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/tactical_challenge.png -------------------------------------------------------------------------------- /assets/EN/goto/tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/goto/tasks.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/battle_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/battle_result.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/best.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/best.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/claim/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/claim/0.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/claim/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/claim/1.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/formation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/formation.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/lose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/lose.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/mobilise.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/mobilise.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/no_tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/no_tick.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/no_tickets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/no_tickets.png -------------------------------------------------------------------------------- /assets/EN/tactical_challenge/win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/assets/EN/tactical_challenge/win.png -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": { 3 | "enabled": false, 4 | "network": "127.0.0.1:5557", 5 | "screenshot_mode": "UIAUTOMATOR2", 6 | "server": "EN", 7 | "restart_attempts": 1, 8 | "auto_start": true, 9 | "launch_emulator": false, 10 | "emulator_path": "", 11 | "delay": 120 12 | }, 13 | "farming": { 14 | "enabled": false, 15 | "bounty": { 16 | "enabled": true, 17 | "overpass": { 18 | "stage": "04", 19 | "run_times": 2 20 | }, 21 | "desert_railroad": { 22 | "stage": "04", 23 | "run_times": 2 24 | }, 25 | "classroom": { 26 | "stage": "05", 27 | "run_times": 2 28 | } 29 | }, 30 | "scrimmage": { 31 | "enabled": true, 32 | "trinity": { 33 | "stage": "01", 34 | "run_times": 3 35 | }, 36 | "gehenna": { 37 | "stage": "03", 38 | "run_times": 3 39 | }, 40 | "millennium": { 41 | "stage": "03", 42 | "run_times": 3 43 | } 44 | }, 45 | "tactical_challenge": { 46 | "enabled": false, 47 | "rank": "lowest" 48 | }, 49 | "mission": { 50 | "enabled": true, 51 | "reset_daily": true, 52 | "last_run": "2023-11-12 20:28:08", 53 | "reset_time": "19:00:00", 54 | "recharge_ap": true, 55 | "preferred_template": "template1", 56 | "queue": [ 57 | [ 58 | "E", 59 | "08", 60 | 19 61 | ], 62 | [ 63 | "E", 64 | "08", 65 | 30 66 | ], 67 | [ 68 | "E", 69 | "30", 70 | 30 71 | ] 72 | ], 73 | "event": true, 74 | "templates": { 75 | "template1": [ 76 | [ 77 | "H", 78 | "5-1", 79 | 3 80 | ], 81 | [ 82 | "H", 83 | "5-3", 84 | 3 85 | ], 86 | [ 87 | "H", 88 | "4-3", 89 | 3 90 | ], 91 | [ 92 | "H", 93 | "3-3", 94 | 3 95 | ], 96 | [ 97 | "H", 98 | "2-2", 99 | 3 100 | ], 101 | [ 102 | "H", 103 | "1-1", 104 | 3 105 | ], 106 | [ 107 | "H", 108 | "1-3", 109 | 3 110 | ], 111 | [ 112 | "E", 113 | "08", 114 | 30 115 | ], 116 | [ 117 | "E", 118 | "08", 119 | 30 120 | ], 121 | [ 122 | "E", 123 | "30", 124 | 30 125 | ] 126 | ] 127 | } 128 | } 129 | }, 130 | "cafe": { 131 | "enabled": false, 132 | "invite_student": false, 133 | "student_name": "Airi", 134 | "tap_students": true, 135 | "claim_earnings": true 136 | }, 137 | "claim_rewards": { 138 | "enabled": false, 139 | "club": true, 140 | "tasks": false, 141 | "mailbox": false 142 | }, 143 | "then": "Exit BAAuto" 144 | } -------------------------------------------------------------------------------- /gui/custom_widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/custom_widgets/__init__.py -------------------------------------------------------------------------------- /gui/custom_widgets/ctk_integerspinbox.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | import re 3 | from typing import Callable 4 | 5 | class CTkIntegerSpinbox(customtkinter.CTkFrame): 6 | def __init__(self, *args, 7 | width: int = 100, 8 | height: int = 32, 9 | step_size: int = 1, 10 | min_value: int = 0, 11 | command: Callable = None, 12 | **kwargs): 13 | super().__init__(*args, width=width, height=height, **kwargs) 14 | 15 | self.step_size = step_size 16 | self.min_value = min_value 17 | self.command = command 18 | 19 | self.grid_columnconfigure((0, 2), weight=0) 20 | self.grid_columnconfigure(1, weight=1) 21 | 22 | self.subtract_button = customtkinter.CTkButton(self, text="-", width=height-6, height=height-6, 23 | command=self.subtract_button_callback) 24 | self.subtract_button.grid(row=0, column=0, padx=(3, 0), pady=3) 25 | 26 | self.entry = customtkinter.CTkEntry(self, width=width-(2*height), height=height-6, border_width=0) 27 | self.entry.grid(row=0, column=1, columnspan=1, padx=3, pady=3, sticky="ew") 28 | 29 | self.add_button = customtkinter.CTkButton(self, text="+", width=height-6, height=height-6, 30 | command=self.add_button_callback) 31 | self.add_button.grid(row=0, column=2, padx=(0, 3), pady=3) 32 | 33 | self.entry.insert(0, "0") 34 | 35 | # Configure validatecommand to allow only integers 36 | vcmd = (self.entry.register(self.validate_input), '%P') 37 | self.entry.configure(validate='key', validatecommand=vcmd) 38 | 39 | def add_button_callback(self): 40 | try: 41 | value = int(self.entry.get()) + self.step_size 42 | self.entry.delete(0, "end") 43 | self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 1 44 | if self.command is not None: 45 | self.command() 46 | except ValueError: 47 | return 48 | 49 | def subtract_button_callback(self): 50 | try: 51 | value = int(self.entry.get()) - self.step_size 52 | self.entry.delete(0, "end") 53 | self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 0 54 | if self.command is not None: 55 | self.command() 56 | except ValueError: 57 | return 58 | 59 | def validate_input(self, new_value): 60 | # Validate that the input is a non-negative integer 61 | return re.match(r'^\d*$', new_value) is not None 62 | 63 | def get(self) -> int: 64 | try: 65 | return int(self.entry.get()) 66 | except ValueError: 67 | return 0 68 | 69 | def set(self, value: int): 70 | self.entry.delete(0, "end") 71 | self.entry.insert(0, max(self.min_value, value)) # Ensure the value is not less than 0 72 | 73 | def configure(self, **kwargs): 74 | state = kwargs.get("state", None) 75 | if state is not None: 76 | self.subtract_button.configure(state=state) 77 | self.add_button.configure(state=state) 78 | self.entry.configure(state=state) 79 | kwargs.pop("state") 80 | super().configure(**kwargs) 81 | -------------------------------------------------------------------------------- /gui/custom_widgets/ctk_notification.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | class CTkNotification(customtkinter.CTkFrame): 3 | def __init__(self, text, master, **kwargs): 4 | super().__init__(master=master, **kwargs) 5 | self.label = customtkinter.CTkLabel(self, text=text, width=200, wraplength=200, font=("Inter", 16)) 6 | self.label.grid(row=0, column=0, sticky="nsew") 7 | self.close_button = customtkinter.CTkButton(self, width=40, text="X", command=self.destroy, fg_color="transparent") 8 | self.close_button.grid(row=0, column=1) 9 | self.progress_bar = customtkinter.CTkProgressBar(self, progress_color="white", determinate_speed=0.4) 10 | self.progress_bar.grid(row=1, column=0, columnspan=2, sticky="nsew") 11 | self.progress_bar.set(0) 12 | self.progress_bar.start() -------------------------------------------------------------------------------- /gui/custom_widgets/ctk_scrollable_dropdown.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Advanced Scrollable Dropdown class for customtkinter widgets 3 | Author: Akash Bora 4 | ''' 5 | 6 | import customtkinter 7 | import sys 8 | import time 9 | 10 | class CTkScrollableDropdown(customtkinter.CTkToplevel): 11 | 12 | def __init__(self, attach, x=None, y=None, button_color=None, height: int = 200, width: int = None, 13 | fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None, 14 | scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, values=[], 15 | command=None, image_values=[], alpha: float = 0.97, frame_corner_radius=20, double_click=False, 16 | resize=True, frame_border_color=None, text_color=None, autocomplete=False, **button_kwargs): 17 | 18 | super().__init__(takefocus=1) 19 | 20 | self.focus() 21 | self.lift() 22 | self.alpha = alpha 23 | self.attach = attach 24 | self.corner = frame_corner_radius 25 | self.padding = 0 26 | self.focus_something = False 27 | self.disable = True 28 | self.update() 29 | 30 | if sys.platform.startswith("win"): 31 | self.after(100, lambda: self.overrideredirect(True)) 32 | self.transparent_color = self._apply_appearance_mode(self._fg_color) 33 | self.attributes("-transparentcolor", self.transparent_color) 34 | elif sys.platform.startswith("darwin"): 35 | self.overrideredirect(True) 36 | self.transparent_color = 'systemTransparent' 37 | self.attributes("-transparent", True) 38 | self.focus_something = True 39 | else: 40 | self.overrideredirect(True) 41 | self.transparent_color = '#000001' 42 | self.corner = 0 43 | self.padding = 18 44 | self.withdraw() 45 | 46 | self.hide = True 47 | self.attach.bind('', lambda e: self._withdraw() if not self.disable else None, add="+") 48 | self.attach.winfo_toplevel().bind('', lambda e: self._withdraw() if not self.disable else None, add="+") 49 | self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") 50 | self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") 51 | self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") 52 | 53 | self.attributes('-alpha', 0) 54 | self.disable = False 55 | self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color 56 | self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color 57 | self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color 58 | self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color 59 | self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color 60 | self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color 61 | 62 | if scrollbar is False: 63 | self.scroll_button_color = self.fg_color 64 | self.scroll_hover_color = self.fg_color 65 | 66 | self.frame = customtkinter.CTkScrollableFrame(self, bg_color=self.transparent_color, fg_color=self.fg_color, 67 | scrollbar_button_hover_color=self.scroll_hover_color, 68 | corner_radius=self.corner, border_width=frame_border_width, 69 | scrollbar_button_color=self.scroll_button_color, 70 | border_color=self.frame_border_color) 71 | self.frame._scrollbar.grid_configure(padx=3) 72 | self.frame.pack(expand=True, fill="both") 73 | self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1) 74 | self.no_match = customtkinter.CTkLabel(self.frame, text="No Match") 75 | self.height = height 76 | self.height_new = height 77 | self.width = width 78 | self.command = command 79 | self.fade = False 80 | self.resize = resize 81 | self.autocomplete = autocomplete 82 | self.var_update = customtkinter.StringVar() 83 | self.appear = False 84 | 85 | if justify.lower()=="left": 86 | self.justify = "w" 87 | elif justify.lower()=="right": 88 | self.justify = "e" 89 | else: 90 | self.justify = "c" 91 | 92 | self.button_height = button_height 93 | self.values = values 94 | self.button_num = len(self.values) 95 | self.image_values = None if len(image_values)!=len(self.values) else image_values 96 | 97 | self.resizable(width=False, height=False) 98 | self.transient(self.master) 99 | self._init_buttons(**button_kwargs) 100 | 101 | # Add binding for different ctk widgets 102 | if double_click or self.attach.winfo_name().startswith("!ctkentry") or self.attach.winfo_name().startswith("!ctkcombobox"): 103 | self.attach.bind('', lambda e: self._iconify(), add="+") 104 | else: 105 | self.attach.bind('', lambda e: self._iconify(), add="+") 106 | 107 | if self.attach.winfo_name().startswith("!ctkcombobox"): 108 | self.attach._canvas.tag_bind("right_parts", "", lambda e: self._iconify()) 109 | self.attach._canvas.tag_bind("dropdown_arrow", "", lambda e: self._iconify()) 110 | if self.command is None: 111 | self.command = self.attach.set 112 | 113 | if self.attach.winfo_name().startswith("!ctkoptionmenu"): 114 | self.attach._canvas.bind("", lambda e: self._iconify()) 115 | self.attach._text_label.bind("", lambda e: self._iconify()) 116 | if self.command is None: 117 | self.command = self.attach.set 118 | 119 | self.attach.bind("", lambda _: self.destroy(), add="+") 120 | 121 | self.update_idletasks() 122 | self.x = x 123 | self.y = y 124 | 125 | if self.autocomplete: 126 | self.bind_autocomplete() 127 | 128 | self.deiconify() 129 | self.withdraw() 130 | 131 | self.attributes("-alpha", self.alpha) 132 | 133 | def _withdraw(self): 134 | self.event_generate("<>") 135 | if self.hide is False: self.withdraw() 136 | self.hide = True 137 | 138 | def _update(self, a, b, c): 139 | self.live_update(self.attach._entry.get().title()) 140 | 141 | def bind_autocomplete(self, ): 142 | def appear(x): 143 | self.appear = True 144 | 145 | if self.attach.winfo_name().startswith("!ctkcombobox"): 146 | self.attach._entry.configure(textvariable=self.var_update) 147 | self.attach._entry.bind("", appear) 148 | self.attach.set(self.values[0]) 149 | self.var_update.trace_add('write', self._update) 150 | 151 | if self.attach.winfo_name().startswith("!ctkentry"): 152 | self.attach.configure(textvariable=self.var_update) 153 | self.attach.bind("", appear) 154 | self.var_update.trace_add('write', self._update) 155 | 156 | def fade_out(self): 157 | for i in range(100,0,-10): 158 | if not self.winfo_exists(): 159 | break 160 | self.attributes("-alpha", i/100) 161 | self.update() 162 | time.sleep(1/100) 163 | 164 | def fade_in(self): 165 | for i in range(0,100,10): 166 | if not self.winfo_exists(): 167 | break 168 | self.attributes("-alpha", i/100) 169 | self.update() 170 | time.sleep(1/100) 171 | 172 | def _init_buttons(self, **button_kwargs): 173 | self.i = 0 174 | self.widgets = {} 175 | for row in self.values: 176 | self.widgets[self.i] = customtkinter.CTkButton(self.frame, 177 | text=row, 178 | height=self.button_height, 179 | fg_color=self.button_color, 180 | text_color=self.text_color, 181 | image=self.image_values[i] if self.image_values is not None else None, 182 | anchor=self.justify, 183 | command=lambda k=row: self._attach_key_press(k), **button_kwargs) 184 | self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) 185 | self.i+=1 186 | 187 | self.hide = False 188 | 189 | def destroy_popup(self): 190 | self.destroy() 191 | self.disable = True 192 | 193 | def place_dropdown(self): 194 | self.x_pos = self.attach.winfo_rootx() if self.x is None else self.x + self.attach.winfo_rootx() 195 | self.y_pos = self.attach.winfo_rooty() + self.attach.winfo_reqheight() + 5 if self.y is None else self.y + self.attach.winfo_rooty() 196 | self.width_new = self.attach.winfo_width() if self.width is None else self.width 197 | 198 | if self.resize: 199 | if self.button_num==1: 200 | self.height_new = self.button_height * self.button_num + 45 201 | else: 202 | self.height_new = self.button_height * self.button_num + 35 203 | if self.height_new>self.height: 204 | self.height_new = self.height 205 | 206 | self.geometry('{}x{}+{}+{}'.format(self.width_new, self.height_new, 207 | self.x_pos, self.y_pos)) 208 | self.fade_in() 209 | self.attributes('-alpha', self.alpha) 210 | self.attach.focus() 211 | 212 | def _iconify(self): 213 | if self.disable: return 214 | if self.hide: 215 | self.event_generate("<>") 216 | self._deiconify() 217 | self.focus() 218 | self.hide = False 219 | self.place_dropdown() 220 | if self.focus_something: 221 | self.dummy_entry.pack() 222 | self.dummy_entry.focus_set() 223 | self.after(100, self.dummy_entry.pack_forget) 224 | else: 225 | self.withdraw() 226 | self.hide = True 227 | 228 | def _attach_key_press(self, k): 229 | self.event_generate("<>") 230 | self.fade = True 231 | if self.command: 232 | self.command(k) 233 | self.fade = False 234 | self.fade_out() 235 | self.withdraw() 236 | self.hide = True 237 | 238 | def live_update(self, string=None): 239 | if not self.appear: return 240 | if self.disable: return 241 | if self.fade: return 242 | if string: 243 | self._deiconify() 244 | i=1 245 | for key in self.widgets.keys(): 246 | s = self.widgets[key].cget("text") 247 | if not s.startswith(string): 248 | self.widgets[key].pack_forget() 249 | else: 250 | self.widgets[key].pack(fill="x", pady=2, padx=(self.padding, 0)) 251 | i+=1 252 | 253 | if i==1: 254 | self.no_match.pack(fill="x", pady=2, padx=(self.padding, 0)) 255 | else: 256 | self.no_match.pack_forget() 257 | self.button_num = i 258 | self.place_dropdown() 259 | 260 | else: 261 | self.no_match.pack_forget() 262 | self.button_num = len(self.values) 263 | for key in self.widgets.keys(): 264 | self.widgets[key].destroy() 265 | self._init_buttons() 266 | self.place_dropdown() 267 | 268 | self.frame._parent_canvas.yview_moveto(0.0) 269 | self.appear = False 270 | 271 | def insert(self, value, **kwargs): 272 | self.widgets[self.i] = customtkinter.CTkButton(self.frame, 273 | text=value, 274 | height=self.button_height, 275 | fg_color=self.button_color, 276 | text_color=self.text_color, 277 | anchor=self.justify, 278 | command=lambda k=value: self._attach_key_press(k), **kwargs) 279 | self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) 280 | self.i+=1 281 | self.values.append(value) 282 | 283 | def _deiconify(self): 284 | if len(self.values)>0: 285 | self.deiconify() 286 | 287 | def popup(self, x=None, y=None): 288 | self.x = x 289 | self.y = y 290 | self.hide = True 291 | self._iconify() 292 | 293 | def configure(self, **kwargs): 294 | if "height" in kwargs: 295 | self.height = kwargs.pop("height") 296 | self.height_new = self.height 297 | 298 | if "alpha" in kwargs: 299 | self.alpha = kwargs.pop("alpha") 300 | 301 | if "width" in kwargs: 302 | self.width = kwargs.pop("width") 303 | 304 | if "fg_color" in kwargs: 305 | self.frame.configure(fg_color=kwargs.pop("fg_color")) 306 | 307 | if "values" in kwargs: 308 | self.values = kwargs.pop("values") 309 | self.image_values = None 310 | for key in self.widgets.keys(): 311 | self.widgets[key].destroy() 312 | self._init_buttons() 313 | 314 | if "image_values" in kwargs: 315 | self.image_values = kwargs.pop("image_values") 316 | self.image_values = None if len(self.image_values)!=len(self.values) else self.image_values 317 | if self.image_values is not None: 318 | i=0 319 | for key in self.widgets.keys(): 320 | self.widgets[key].configure(image=self.image_values[i]) 321 | i+=1 322 | 323 | if "button_color" in kwargs: 324 | for key in self.widgets.keys(): 325 | self.widgets[key].configure(fg_color=kwargs.pop("button_color")) 326 | 327 | for key in self.widgets.keys(): 328 | self.widgets[key].configure(**kwargs) 329 | -------------------------------------------------------------------------------- /gui/custom_widgets/ctk_scrollable_dropdown_frame.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Advanced Scrollable Dropdown Frame class for customtkinter widgets 3 | Author: Akash Bora 4 | ''' 5 | 6 | import customtkinter 7 | import sys 8 | 9 | class CTkScrollableDropdownFrame(customtkinter.CTkFrame): 10 | 11 | def __init__(self, attach, x=None, y=None, button_color=None, height: int = 200, width: int = None, 12 | fg_color=None, button_height: int = 20, justify="center", scrollbar_button_color=None, 13 | scrollbar=True, scrollbar_button_hover_color=None, frame_border_width=2, values=[], 14 | command=None, image_values=[], double_click=False, frame_corner_radius=True, resize=True, frame_border_color=None, 15 | text_color=None, autocomplete=False, **button_kwargs): 16 | 17 | super().__init__(master=attach.winfo_toplevel(), bg_color=attach.cget("bg_color")) 18 | 19 | self.attach = attach 20 | self.corner = 11 if frame_corner_radius else 0 21 | self.padding = 0 22 | self.disable = True 23 | 24 | self.hide = True 25 | self.attach.bind('', lambda e: self._withdraw() if not self.disable else None, add="+") 26 | self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") 27 | self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") 28 | self.attach.winfo_toplevel().bind("", lambda e: self._withdraw() if not self.disable else None, add="+") 29 | 30 | self.disable = False 31 | self.fg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if fg_color is None else fg_color 32 | self.scroll_button_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_color"] if scrollbar_button_color is None else scrollbar_button_color 33 | self.scroll_hover_color = customtkinter.ThemeManager.theme["CTkScrollbar"]["button_hover_color"] if scrollbar_button_hover_color is None else scrollbar_button_hover_color 34 | self.frame_border_color = customtkinter.ThemeManager.theme["CTkFrame"]["border_color"] if frame_border_color is None else frame_border_color 35 | self.button_color = customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"] if button_color is None else button_color 36 | self.text_color = customtkinter.ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else text_color 37 | 38 | if scrollbar is False: 39 | self.scroll_button_color = self.fg_color 40 | self.scroll_hover_color = self.fg_color 41 | 42 | self.frame = customtkinter.CTkScrollableFrame(self, fg_color=self.fg_color, bg_color=attach.cget("bg_color"), 43 | scrollbar_button_hover_color=self.scroll_hover_color, 44 | corner_radius=self.corner, border_width=frame_border_width, 45 | scrollbar_button_color=self.scroll_button_color, 46 | border_color=self.frame_border_color) 47 | self.frame._scrollbar.grid_configure(padx=3) 48 | self.frame.pack(expand=True, fill="both") 49 | 50 | if self.corner==0: 51 | self.corner = 21 52 | 53 | self.dummy_entry = customtkinter.CTkEntry(self.frame, fg_color="transparent", border_width=0, height=1, width=1) 54 | self.no_match = customtkinter.CTkLabel(self.frame, text="No Match") 55 | self.height = height 56 | self.height_new = height 57 | self.width = width 58 | self.command = command 59 | self.fade = False 60 | self.resize = resize 61 | self.autocomplete = autocomplete 62 | self.var_update = customtkinter.StringVar() 63 | self.appear = False 64 | 65 | if justify.lower()=="left": 66 | self.justify = "w" 67 | elif justify.lower()=="right": 68 | self.justify = "e" 69 | else: 70 | self.justify = "c" 71 | 72 | self.button_height = button_height 73 | self.values = values 74 | self.button_num = len(self.values) 75 | self.image_values = None if len(image_values)!=len(self.values) else image_values 76 | 77 | self._init_buttons(**button_kwargs) 78 | 79 | # Add binding for different ctk widgets 80 | if double_click or self.attach.winfo_name().startswith("!ctkentry") or self.attach.winfo_name().startswith("!ctkcombobox"): 81 | self.attach.bind('', lambda e: self._iconify(), add="+") 82 | self.attach._entry.bind('', lambda e: self._withdraw() if not self.disable else None, add="+") 83 | else: 84 | self.attach.bind('', lambda e: self._iconify(), add="+") 85 | 86 | if self.attach.winfo_name().startswith("!ctkcombobox"): 87 | self.attach._canvas.tag_bind("right_parts", "", lambda e: self._iconify()) 88 | self.attach._canvas.tag_bind("dropdown_arrow", "", lambda e: self._iconify()) 89 | 90 | if self.command is None: 91 | self.command = self.attach.set 92 | 93 | if self.attach.winfo_name().startswith("!ctkoptionmenu"): 94 | self.attach._canvas.bind("", lambda e: self._iconify()) 95 | self.attach._text_label.bind("", lambda e: self._iconify()) 96 | if self.command is None: 97 | self.command = self.attach.set 98 | 99 | self.x = x 100 | self.y = y 101 | 102 | self.attach.bind("", lambda _: self.destroy(), add="+") 103 | 104 | if self.autocomplete: 105 | self.bind_autocomplete() 106 | 107 | def _withdraw(self): 108 | self.event_generate("<>") 109 | if self.hide is False: self.place_forget() 110 | self.hide = True 111 | 112 | def _update(self, a, b, c): 113 | self.live_update(self.attach._entry.get()) 114 | 115 | def bind_autocomplete(self, ): 116 | def appear(x): 117 | self.appear = True 118 | 119 | if self.attach.winfo_name().startswith("!ctkcombobox"): 120 | self.attach._entry.configure(textvariable=self.var_update) 121 | self.attach.set(self.values[0]) 122 | self.attach._entry.bind("", appear) 123 | self.var_update.trace_add('write', self._update) 124 | 125 | if self.attach.winfo_name().startswith("!ctkentry"): 126 | self.attach.configure(textvariable=self.var_update) 127 | self.attach.bind("", appear) 128 | self.var_update.trace_add('write', self._update) 129 | 130 | def _init_buttons(self, **button_kwargs): 131 | self.i = 0 132 | self.widgets = {} 133 | for row in self.values: 134 | self.widgets[self.i] = customtkinter.CTkButton(self.frame, 135 | text=row, 136 | height=self.button_height, 137 | fg_color=self.button_color, 138 | text_color=self.text_color, 139 | image=self.image_values[i] if self.image_values is not None else None, 140 | anchor=self.justify, 141 | command=lambda k=row: self._attach_key_press(k), **button_kwargs) 142 | self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) 143 | self.i+=1 144 | 145 | self.hide = False 146 | 147 | def destroy_popup(self): 148 | self.destroy() 149 | self.disable = True 150 | 151 | def place_dropdown(self): 152 | self.x_pos = self.attach.winfo_x() if self.x is None else self.x + self.attach.winfo_rootx() 153 | self.y_pos = self.attach.winfo_y() + self.attach.winfo_reqheight() + 5 if self.y is None else self.y + self.attach.winfo_rooty() 154 | self.width_new = self.attach.winfo_width()-45+self.corner if self.width is None else self.width 155 | 156 | if self.resize: 157 | if self.button_num==1: 158 | self.height_new = self.button_height * self.button_num + 45 159 | else: 160 | self.height_new = self.button_height * self.button_num + 35 161 | if self.height_new>self.height: 162 | self.height_new = self.height 163 | 164 | self.frame.configure(width=self.width_new, height=self.height_new) 165 | self.place(x=self.x_pos, y=self.y_pos) 166 | 167 | if sys.platform.startswith("darwin"): 168 | self.dummy_entry.pack() 169 | self.after(100, self.dummy_entry.pack_forget()) 170 | 171 | self.lift() 172 | self.attach.focus() 173 | 174 | def _iconify(self): 175 | if self.disable: return 176 | if self.hide: 177 | self.event_generate("<>") 178 | self.hide = False 179 | self.place_dropdown() 180 | else: 181 | self.place_forget() 182 | self.hide = True 183 | 184 | def _attach_key_press(self, k): 185 | self.event_generate("<>") 186 | self.fade = True 187 | if self.command: 188 | self.command(k) 189 | self.fade = False 190 | self.place_forget() 191 | self.hide = True 192 | 193 | def live_update(self, string=None): 194 | if not self.appear: return 195 | if self.disable: return 196 | if self.fade: return 197 | if string: 198 | self._deiconify() 199 | i=1 200 | for key in self.widgets.keys(): 201 | s = self.widgets[key].cget("text") 202 | if not s.startswith(string): 203 | self.widgets[key].pack_forget() 204 | else: 205 | self.widgets[key].pack(fill="x", pady=2, padx=(self.padding, 0)) 206 | i+=1 207 | 208 | if i==1: 209 | self.no_match.pack(fill="x", pady=2, padx=(self.padding, 0)) 210 | else: 211 | self.no_match.pack_forget() 212 | self.button_num = i 213 | self.place_dropdown() 214 | 215 | else: 216 | self.no_match.pack_forget() 217 | self.button_num = len(self.values) 218 | for key in self.widgets.keys(): 219 | self.widgets[key].destroy() 220 | self._init_buttons() 221 | self.place_dropdown() 222 | 223 | self.frame._parent_canvas.yview_moveto(0.0) 224 | self.appear = False 225 | 226 | def insert(self, value, **kwargs): 227 | self.widgets[self.i] = customtkinter.CTkButton(self.frame, 228 | text=value, 229 | height=self.button_height, 230 | fg_color=self.button_color, 231 | text_color=self.text_color, 232 | anchor=self.justify, 233 | command=lambda k=value: self._attach_key_press(k), **kwargs) 234 | self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) 235 | self.i+=1 236 | self.values.append(value) 237 | 238 | def _deiconify(self): 239 | if len(self.values)>0: 240 | self.pack_forget() 241 | 242 | def popup(self, x=None, y=None): 243 | self.x = x 244 | self.y = y 245 | self.hide = True 246 | self._iconify() 247 | 248 | def configure(self, **kwargs): 249 | if "height" in kwargs: 250 | self.height = kwargs.pop("height") 251 | self.height_new = self.height 252 | 253 | if "alpha" in kwargs: 254 | self.alpha = kwargs.pop("alpha") 255 | 256 | if "width" in kwargs: 257 | self.width = kwargs.pop("width") 258 | 259 | if "fg_color" in kwargs: 260 | self.frame.configure(fg_color=kwargs.pop("fg_color")) 261 | 262 | if "values" in kwargs: 263 | self.values = kwargs.pop("values") 264 | self.image_values = None 265 | for key in self.widgets.keys(): 266 | self.widgets[key].destroy() 267 | self._init_buttons() 268 | 269 | if "image_values" in kwargs: 270 | self.image_values = kwargs.pop("image_values") 271 | self.image_values = None if len(self.image_values)!=len(self.values) else self.image_values 272 | if self.image_values is not None: 273 | i=0 274 | for key in self.widgets.keys(): 275 | self.widgets[key].configure(image=self.image_values[i]) 276 | i+=1 277 | 278 | if "button_color" in kwargs: 279 | for key in self.widgets.keys(): 280 | self.widgets[key].configure(fg_color=kwargs.pop("button_color")) 281 | 282 | for key in self.widgets.keys(): 283 | self.widgets[key].configure(**kwargs) 284 | -------------------------------------------------------------------------------- /gui/custom_widgets/ctk_templatedialog.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Tuple, Optional 2 | from customtkinter import CTkLabel, CTkEntry, CTkButton, ThemeManager, CTkToplevel, CTkFont, CTkOptionMenu 3 | 4 | 5 | class CTkTemplateDialog(CTkToplevel): 6 | """ 7 | Dialog with extra window, message, entry widget, cancel and ok button. 8 | For detailed information check out the documentation. 9 | """ 10 | 11 | def __init__(self, 12 | fg_color: Optional[Union[str, Tuple[str, str]]] = None, 13 | text_color: Optional[Union[str, Tuple[str, str]]] = None, 14 | button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, 15 | button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, 16 | button_text_color: Optional[Union[str, Tuple[str, str]]] = None, 17 | entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None, 18 | entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, 19 | entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, 20 | 21 | title: str = "CTkDialog", 22 | font: Optional[Union[tuple, CTkFont]] = None, 23 | text: str = "CTkDialog", 24 | values = []): 25 | 26 | super().__init__(fg_color=fg_color) 27 | 28 | self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) 29 | self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color) 30 | self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color) 31 | self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) 32 | self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color) 33 | self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color) 34 | self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color) 35 | self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color) 36 | 37 | self._user_input = ("", "") 38 | self._running: bool = False 39 | self._title = title 40 | self._text = text 41 | self._font = font 42 | self._values = [""] + values 43 | 44 | self.title(self._title) 45 | self.lift() # lift window on top 46 | self.attributes("-topmost", True) # stay on top 47 | self.protocol("WM_DELETE_WINDOW", self._on_closing) 48 | self.after(10, self._create_widgets) # create widgets with slight delay, to avoid white flickering of background 49 | self.resizable(False, False) 50 | self.grab_set() # make other windows not clickable 51 | 52 | def _create_widgets(self): 53 | self.grid_columnconfigure((0, 1), weight=1) 54 | self.rowconfigure(0, weight=1) 55 | 56 | self._label = CTkLabel(master=self, 57 | width=300, 58 | wraplength=300, 59 | fg_color="transparent", 60 | text_color=self._text_color, 61 | text=self._text, 62 | font=self._font) 63 | self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew") 64 | 65 | self._entry = CTkEntry(master=self, 66 | width=230, 67 | fg_color=self._entry_fg_color, 68 | border_color=self._entry_border_color, 69 | text_color=self._entry_text_color, 70 | font=self._font) 71 | self._entry.grid(row=1, column=0, columnspan=2, padx=20, pady=(0, 20), sticky="ew") 72 | 73 | self._label2 = CTkLabel(master=self, 74 | width=100, 75 | wraplength=100, 76 | fg_color="transparent", 77 | text_color=self._text_color, 78 | text="Import stages from: ", 79 | font=self._font) 80 | self._label2.grid(row=2, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew") 81 | 82 | self._template_optionmenu = CTkOptionMenu(master=self, 83 | width=100, 84 | fg_color=self._button_fg_color, 85 | button_hover_color=self._button_hover_color, 86 | text_color=self._button_text_color, 87 | font=self._font, 88 | values=self._values 89 | ) 90 | self._template_optionmenu.grid(row=2, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew") 91 | 92 | 93 | self._ok_button = CTkButton(master=self, 94 | width=100, 95 | border_width=0, 96 | fg_color=self._button_fg_color, 97 | hover_color=self._button_hover_color, 98 | text_color=self._button_text_color, 99 | text='Ok', 100 | font=self._font, 101 | command=self._ok_event) 102 | self._ok_button.grid(row=3, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew") 103 | 104 | self._cancel_button = CTkButton(master=self, 105 | width=100, 106 | border_width=0, 107 | fg_color=self._button_fg_color, 108 | hover_color=self._button_hover_color, 109 | text_color=self._button_text_color, 110 | text='Cancel', 111 | font=self._font, 112 | command=self._cancel_event) 113 | self._cancel_button.grid(row=3, column=1, columnspan=1, padx=(10, 20), pady=(0, 20), sticky="ew") 114 | 115 | self.after(150, lambda: self._entry.focus()) # set focus to entry with slight delay, otherwise it won't work 116 | self._entry.bind("", self._ok_event) 117 | 118 | def _ok_event(self, event=None): 119 | self._user_input = self._entry.get(), self._template_optionmenu.get() 120 | self.grab_release() 121 | self.destroy() 122 | 123 | def _on_closing(self): 124 | self.grab_release() 125 | self.destroy() 126 | 127 | def _cancel_event(self): 128 | self.grab_release() 129 | self.destroy() 130 | 131 | def get_input(self): 132 | self.master.wait_window(self) 133 | return self._user_input 134 | -------------------------------------------------------------------------------- /gui/custom_widgets/ctk_timeentry.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | import tkinter as tk 3 | 4 | class CTkTimeEntry(customtkinter.CTkFrame): 5 | def __init__(self, master=None, **kwargs): 6 | super().__init__(master, **kwargs) 7 | self.hour = tk.StringVar() 8 | self.minute = tk.StringVar() 9 | self.second = tk.StringVar() 10 | 11 | self.hour_entry = customtkinter.CTkEntry(self, width=50, textvariable=self.hour, validate="key", validatecommand=(self.register(self.validate_hour), '%P')) 12 | self.hour_entry.pack(side=tk.LEFT) 13 | 14 | self.minute_entry = customtkinter.CTkEntry(self,width=50, textvariable=self.minute, validate="key", validatecommand=(self.register(self.validate_min_sec), '%P')) 15 | self.minute_entry.pack(side=tk.LEFT) 16 | 17 | self.second_entry = customtkinter.CTkEntry(self, width=50, textvariable=self.second, validate="key", validatecommand=(self.register(self.validate_min_sec), '%P')) 18 | self.second_entry.pack(side=tk.LEFT) 19 | 20 | def validate_hour(self, P): 21 | return len(P) <= 2 and (P.isdigit() and int(P) <= 23 or P == "") 22 | 23 | def validate_min_sec(self, P): 24 | return len(P) <= 2 and (P.isdigit() and int(P) <= 59 or P == "") 25 | 26 | def set(self, time_str): 27 | h, m, s = map(str, time_str.split(':')) 28 | self.hour.set(h) 29 | self.minute.set(m) 30 | self.second.set(s) 31 | 32 | def get(self): 33 | h = self.hour.get() if self.hour.get() else "00" 34 | m = self.minute.get() if self.minute.get() else "00" 35 | s = self.second.get() if self.second.get() else "00" 36 | return f"{h.zfill(2)}:{m.zfill(2)}:{s.zfill(2)}" -------------------------------------------------------------------------------- /gui/custom_widgets/ctk_tooltip.py: -------------------------------------------------------------------------------- 1 | """ 2 | CTkToolTip Widget 3 | version: 0.8 4 | """ 5 | 6 | import time 7 | import sys 8 | import customtkinter 9 | from tkinter import Toplevel, Frame 10 | 11 | 12 | class CTkToolTip(Toplevel): 13 | """ 14 | Creates a ToolTip (pop-up) widget for customtkinter. 15 | """ 16 | 17 | def __init__( 18 | self, 19 | widget: any = None, 20 | message: str = None, 21 | delay: float = 0.2, 22 | follow: bool = True, 23 | x_offset: int = +20, 24 | y_offset: int = +10, 25 | bg_color: str = None, 26 | corner_radius: int = 10, 27 | border_width: int = 0, 28 | border_color: str = None, 29 | alpha: float = 0.95, 30 | padding: tuple = (10, 2), 31 | **message_kwargs): 32 | 33 | super().__init__() 34 | 35 | self.widget = widget 36 | 37 | self.withdraw() 38 | 39 | # Disable ToolTip's title bar 40 | self.overrideredirect(True) 41 | 42 | if sys.platform.startswith("win"): 43 | self.transparent_color = self.widget._apply_appearance_mode( 44 | customtkinter.ThemeManager.theme["CTkToplevel"]["fg_color"]) 45 | self.attributes("-transparentcolor", self.transparent_color) 46 | self.transient() 47 | elif sys.platform.startswith("darwin"): 48 | self.transparent_color = 'systemTransparent' 49 | self.attributes("-transparent", True) 50 | self.transient(self.master) 51 | else: 52 | self.transparent_color = '#000001' 53 | corner_radius = 0 54 | self.transient() 55 | 56 | self.resizable(width=True, height=True) 57 | 58 | # Make the background transparent 59 | self.config(background=self.transparent_color) 60 | 61 | # StringVar instance for msg string 62 | self.messageVar = customtkinter.StringVar() 63 | self.message = message 64 | self.messageVar.set(self.message) 65 | 66 | self.delay = delay 67 | self.follow = follow 68 | self.x_offset = x_offset 69 | self.y_offset = y_offset 70 | self.corner_radius = corner_radius 71 | self.alpha = alpha 72 | self.border_width = border_width 73 | self.padding = padding 74 | self.bg_color = customtkinter.ThemeManager.theme["CTkFrame"]["fg_color"] if bg_color is None else bg_color 75 | self.border_color = border_color 76 | self.disable = False 77 | 78 | # visibility status of the ToolTip inside|outside|visible 79 | self.status = "outside" 80 | self.last_moved = 0 81 | self.attributes('-alpha', self.alpha) 82 | 83 | if sys.platform.startswith("win"): 84 | if self.widget._apply_appearance_mode(self.bg_color) == self.transparent_color: 85 | self.transparent_color = "#000001" 86 | self.config(background=self.transparent_color) 87 | self.attributes("-transparentcolor", self.transparent_color) 88 | 89 | # Add the message widget inside the tooltip 90 | self.transparent_frame = Frame(self, bg=self.transparent_color) 91 | self.transparent_frame.pack(padx=0, pady=0, fill="both", expand=True) 92 | 93 | self.frame = customtkinter.CTkFrame(self.transparent_frame, bg_color=self.transparent_color, 94 | corner_radius=self.corner_radius, 95 | border_width=self.border_width, fg_color=self.bg_color, 96 | border_color=self.border_color) 97 | self.frame.pack(padx=0, pady=0, fill="both", expand=True) 98 | 99 | self.message_label = customtkinter.CTkLabel(self.frame, textvariable=self.messageVar, **message_kwargs) 100 | self.message_label.pack(fill="both", padx=self.padding[0] + self.border_width, 101 | pady=self.padding[1] + self.border_width, expand=True) 102 | 103 | if self.widget.winfo_name() != "tk": 104 | if self.frame.cget("fg_color") == self.widget.cget("bg_color"): 105 | if not bg_color: 106 | self._top_fg_color = self.frame._apply_appearance_mode( 107 | customtkinter.ThemeManager.theme["CTkFrame"]["top_fg_color"]) 108 | if self._top_fg_color != self.transparent_color: 109 | self.frame.configure(fg_color=self._top_fg_color) 110 | 111 | # Add bindings to the widget without overriding the existing ones 112 | self.widget.bind("", self.on_enter, add="+") 113 | self.widget.bind("", self.on_leave, add="+") 114 | self.widget.bind("", self.on_enter, add="+") 115 | self.widget.bind("", self.on_enter, add="+") 116 | self.widget.bind("", lambda _: self.hide(), add="+") 117 | 118 | def show(self) -> None: 119 | """ 120 | Enable the widget. 121 | """ 122 | self.disable = False 123 | 124 | def on_enter(self, event) -> None: 125 | """ 126 | Processes motion within the widget including entering and moving. 127 | """ 128 | 129 | if self.disable: 130 | return 131 | self.last_moved = time.time() 132 | 133 | # Set the status as inside for the very first time 134 | if self.status == "outside": 135 | self.status = "inside" 136 | 137 | # If the follow flag is not set, motion within the widget will make the ToolTip dissapear 138 | if not self.follow: 139 | self.status = "inside" 140 | self.withdraw() 141 | 142 | # Calculate available space on the right side of the widget relative to the screen 143 | root_width = self.winfo_screenwidth() 144 | widget_x = event.x_root 145 | space_on_right = root_width - widget_x 146 | 147 | # Calculate the width of the tooltip's text based on the length of the message string 148 | text_width = self.message_label.winfo_reqwidth() 149 | 150 | # Calculate the offset based on available space and text width to avoid going off-screen on the right side 151 | offset_x = self.x_offset 152 | if space_on_right < text_width + 20: # Adjust the threshold as needed 153 | offset_x = -text_width - 20 # Negative offset when space is limited on the right side 154 | 155 | # Offsets the ToolTip using the coordinates od an event as an origin 156 | self.geometry(f"+{event.x_root + offset_x}+{event.y_root + self.y_offset}") 157 | 158 | # Time is in integer: milliseconds 159 | self.after(int(self.delay * 1000), self._show) 160 | 161 | def on_leave(self, event=None) -> None: 162 | """ 163 | Hides the ToolTip temporarily. 164 | """ 165 | 166 | if self.disable: return 167 | self.status = "outside" 168 | self.withdraw() 169 | 170 | def _show(self) -> None: 171 | """ 172 | Displays the ToolTip. 173 | """ 174 | 175 | if not self.widget.winfo_exists(): 176 | self.hide() 177 | self.destroy() 178 | 179 | if self.status == "inside" and time.time() - self.last_moved >= self.delay: 180 | self.status = "visible" 181 | self.deiconify() 182 | 183 | def hide(self) -> None: 184 | """ 185 | Disable the widget from appearing. 186 | """ 187 | if not self.winfo_exists(): 188 | return 189 | self.withdraw() 190 | self.disable = True 191 | 192 | def is_disabled(self) -> None: 193 | """ 194 | Return the window state 195 | """ 196 | return self.disable 197 | 198 | def get(self) -> None: 199 | """ 200 | Returns the text on the tooltip. 201 | """ 202 | return self.messageVar.get() 203 | 204 | def configure(self, message: str = None, delay: float = None, bg_color: str = None, **kwargs): 205 | """ 206 | Set new message or configure the label parameters. 207 | """ 208 | if delay: self.delay = delay 209 | if bg_color: self.frame.configure(fg_color=bg_color) 210 | 211 | self.messageVar.set(message) 212 | self.message_label.configure(**kwargs) 213 | -------------------------------------------------------------------------------- /gui/custom_widgets/icons/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/custom_widgets/icons/cancel.png -------------------------------------------------------------------------------- /gui/custom_widgets/icons/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/custom_widgets/icons/check.png -------------------------------------------------------------------------------- /gui/custom_widgets/icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/custom_widgets/icons/info.png -------------------------------------------------------------------------------- /gui/custom_widgets/icons/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/custom_widgets/icons/question.png -------------------------------------------------------------------------------- /gui/custom_widgets/icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/custom_widgets/icons/warning.png -------------------------------------------------------------------------------- /gui/frames/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/frames/__init__.py -------------------------------------------------------------------------------- /gui/frames/cafe.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | import json 3 | from gui.custom_widgets.ctk_scrollable_dropdown import CTkScrollableDropdown 4 | 5 | class CafeFrame(customtkinter.CTkFrame): 6 | def __init__(self, master, linker, config, **kwargs): 7 | super().__init__(master, **kwargs) 8 | self.linker = linker 9 | self.config = config 10 | self.create_widgets() 11 | 12 | def create_widgets(self): 13 | self.create_cafe_settings_label() 14 | self.create_invite_student_widgets() 15 | self.create_tap_students_widgets() 16 | self.create_claim_earnings_widgets() 17 | 18 | def create_cafe_settings_label(self): 19 | self.cafe_settings_label = customtkinter.CTkLabel(self, text="Cafe Settings", font=customtkinter.CTkFont(family="Inter", size=30, weight="bold")) 20 | self.cafe_settings_label.grid(row=0, column=0, sticky="nw", padx=20, pady=20) 21 | 22 | def create_invite_student_widgets(self): 23 | self.invite_checkbox = customtkinter.CTkCheckBox(self, text="Invite student", font=customtkinter.CTkFont(family="Inter", size=20), command=lambda x=["cafe", "invite_student"]: self.config.save_to_json(x)) 24 | self.invite_checkbox.grid(row=1, column=0, pady=(20, 0), padx=20, sticky="nw") 25 | 26 | self.student_entry = customtkinter.CTkComboBox(self, width=180) 27 | self.student_entry.bind("", lambda event, x=["cafe", "student_name"]: (self.config.save_to_json(x))) 28 | self.student_entry.grid(row=1, column=1, padx=20, pady=(20, 0)) 29 | 30 | save_student = lambda name: self.student_entry.set(name) 31 | server = self.config.config_data["login"]["server"] 32 | with open(f"gui/student_list/{server}.json", "r") as f: 33 | student_list = json.load(f) 34 | self.student_dropdown = CTkScrollableDropdown(self.student_entry, values=student_list, width=180, height=550, autocomplete=True, command=lambda choice, x=["cafe", "student_name"]: (save_student(choice), self.config.save_to_json(x))) 35 | self.linker.widgets["cafe"]["invite_student"] = self.invite_checkbox 36 | self.linker.widgets["cafe"]["student_name"] = self.student_entry 37 | 38 | def create_tap_students_widgets(self): 39 | self.tap_checkbox = customtkinter.CTkCheckBox(self, text="Tap Students", font=customtkinter.CTkFont(family="Inter", size=20), command=lambda x=["cafe", "tap_students"]: self.config.save_to_json(x)) 40 | self.tap_checkbox.grid(row=2, column=0, pady=(20,0), padx=20, sticky="nw") 41 | self.linker.widgets["cafe"]["tap_students"] = self.tap_checkbox 42 | 43 | 44 | def create_claim_earnings_widgets(self): 45 | self.claim_checkbox = customtkinter.CTkCheckBox(self, text="Claim Earnings", font=customtkinter.CTkFont(family="Inter", size=20), command=lambda x=["cafe", "claim_earnings"]: self.config.save_to_json(x)) 46 | self.claim_checkbox.grid(row=3, column=0, pady=(20, 0), padx=20, sticky="nw") 47 | self.linker.widgets["cafe"]["claim_earnings"] = self.claim_checkbox 48 | -------------------------------------------------------------------------------- /gui/frames/claim_rewards.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | 3 | class ClaimRewardsFrame(customtkinter.CTkFrame): 4 | def __init__(self, master, linker, config, **kwargs): 5 | super().__init__(master, **kwargs) 6 | self.linker = linker 7 | self.config = config 8 | self.claim_reward_settings_label = customtkinter.CTkLabel(self, text="Claim Rewards Settings", font=customtkinter.CTkFont(family="Inter", size=30, weight="bold")) 9 | self.claim_reward_settings_label.grid(row=0, column=0, sticky="nw", padx=20, pady=20) 10 | self.claim_club_checkbox = customtkinter.CTkCheckBox(self, text="Club", font=customtkinter.CTkFont(family="Inter", size=20, weight="bold"), command= lambda x=["claim_rewards", "club"]: self.config.save_to_json(x)) 11 | self.claim_club_checkbox.grid(row=1, column=0, pady=(20, 0), padx=20, sticky="nw") 12 | self.claim_tasks_checkbox = customtkinter.CTkCheckBox(self, text="Tasks", font=customtkinter.CTkFont(family="Inter", size=20, weight="bold"), command= lambda x=["claim_rewards", "tasks"]: self.config.save_to_json(x)) 13 | self.claim_tasks_checkbox.grid(row=2, column=0, pady=(20, 0), padx=20, sticky="nw") 14 | self.claim_mail_checkbox = customtkinter.CTkCheckBox(self, text="Mailbox", font=customtkinter.CTkFont(family="Inter", size=20, weight="bold"), command= lambda x=["claim_rewards", "mailbox"]: self.config.save_to_json(x)) 15 | self.claim_mail_checkbox.grid(row=3, column=0, pady=(20, 0), padx=20, sticky="nw") 16 | self.linker.widgets["claim_rewards"]["club"] = self.claim_club_checkbox 17 | self.linker.widgets["claim_rewards"]["tasks"] = self.claim_tasks_checkbox 18 | self.linker.widgets["claim_rewards"]["mailbox"] = self.claim_mail_checkbox -------------------------------------------------------------------------------- /gui/frames/logger.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | 3 | class LoggerTextBox(customtkinter.CTkFrame): 4 | def __init__(self, master, linker, config, **kwargs): 5 | super().__init__(master=master, **kwargs) 6 | self.linker = linker 7 | self.config = config 8 | self.grid_rowconfigure(0, weight=0) 9 | self.grid_rowconfigure(1, weight=1) 10 | self.grid_columnconfigure(0, weight=1) 11 | 12 | self.log_label = customtkinter.CTkLabel(self, text="Log",font=customtkinter.CTkFont(family="Inter", size=30, weight="bold")) 13 | self.log_label.grid(row=0, column=0, sticky="nw", padx=20, pady=(20,0)) 14 | # Button to toggle autoscroll 15 | self.toggle_autoscroll_button = customtkinter.CTkButton(self, height=35, text="Autoscroll On", command=self.toggle_autoscroll, font=("Inter", 16)) 16 | self.toggle_autoscroll_button.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") 17 | self.autoscroll_enabled = True # Initially, autoscroll is enabled 18 | self.log_textbox = customtkinter.CTkTextbox(self, state="disabled", font=("Inter", 16), wrap="word") 19 | self.log_textbox.grid(row=1, column=0,columnspan=4, padx=20, pady=20, sticky="nsew") 20 | self.log_level_colors = { 21 | "[INFO]": "light blue", 22 | "[SUCCESS]": "light green", 23 | "[WARNING]": "orange", 24 | "[ERROR]": "red", 25 | "[DEBUG]": "purple" 26 | } 27 | for level, color in self.log_level_colors.items(): 28 | self.log_textbox.tag_config(level, foreground=color) 29 | self.linker.logger = self 30 | 31 | def toggle_autoscroll(self): 32 | self.autoscroll_enabled = not self.autoscroll_enabled 33 | if self.autoscroll_enabled: 34 | self.toggle_autoscroll_button.configure(text="Autoscroll On") 35 | else: 36 | self.toggle_autoscroll_button.configure(text="Autoscroll Off") -------------------------------------------------------------------------------- /gui/frames/login.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | from gui.custom_widgets.ctk_tooltip import CTkToolTip 3 | from gui.custom_widgets.ctk_integerspinbox import CTkIntegerSpinbox 4 | from tkinter import filedialog, END 5 | 6 | class LoginFrame(customtkinter.CTkFrame): 7 | def __init__(self, master, linker, config, **kwargs): 8 | super().__init__(master, **kwargs) 9 | self.linker = linker 10 | self.config = config 11 | self.create_widgets() 12 | 13 | def create_widgets(self): 14 | self.login_settings_label = customtkinter.CTkLabel(self, text="Login Settings", font=customtkinter.CTkFont(family="Inter", size=30, weight="bold")) 15 | self.login_settings_label.grid(row=0, column=0, sticky="nw", padx=20, pady=20) 16 | 17 | self.core_label = customtkinter.CTkLabel(self, text="Core", font=customtkinter.CTkFont(family="Inter", size=26, weight="bold")) 18 | self.core_label.grid(row=1, column=0, sticky="nw", padx=20, pady=20) 19 | 20 | self.create_network_widgets() 21 | self.create_screenshot_widgets() 22 | self.create_server_widgets() 23 | self.create_restart_attempts_widgets() 24 | self.create_autorun_widgets() 25 | self.create_emulator_widgets() 26 | 27 | def create_network_widgets(self): 28 | self.network_label = customtkinter.CTkLabel(self, text="Connection address:", font=customtkinter.CTkFont(family="Inter", size=20)) 29 | self.network_label.grid(row=2, column=0, padx=20, pady=(20, 10)) 30 | 31 | self.network_entry = customtkinter.CTkEntry(self) 32 | self.network_entry.bind("", lambda event, x=["login", "network"]: self.config.save_to_json(x)) 33 | self.network_entry.grid(row=2, column=1, padx=20, pady=(20, 10)) 34 | 35 | self.linker.widgets["login"]["network"] = self.network_entry 36 | 37 | def create_screenshot_widgets(self): 38 | self.screenshot_label = customtkinter.CTkLabel(self, text="Screenshot mode:", font=customtkinter.CTkFont(size=20)) 39 | self.screenshot_label.grid(row=3, column=0, padx=20, pady=(20, 10)) 40 | 41 | self.screenshot_dropdown = customtkinter.CTkOptionMenu(self, values=["SCREENCAP_PNG", "SCREENCAP_RAW", "ASCREENCAP", "UIAUTOMATOR2"], command=lambda x, y=["login", "screenshot_mode"]: self.config.save_to_json(y)) 42 | self.screenshot_dropdown.grid(row=3, column=1, padx=20, pady=(20, 10)) 43 | 44 | self.linker.widgets["login"]["screenshot_mode"] = self.screenshot_dropdown 45 | 46 | def create_server_widgets(self): 47 | self.server_label = customtkinter.CTkLabel(self, text="Server:", font=customtkinter.CTkFont(size=20)) 48 | self.server_label.grid(row=4, column=0, padx=20, pady=(20, 10)) 49 | 50 | self.server_dropdown = customtkinter.CTkOptionMenu(self, values=["EN", "CN"], command=lambda x,y=["login","server"]: (self.config.save_to_json(y), self.linker.switch_student_list())) 51 | self.server_dropdown.grid(row=4, column=1, padx=20, pady=(20, 10)) 52 | 53 | self.linker.widgets["login"]["server"] = self.server_dropdown 54 | 55 | def create_restart_attempts_widgets(self): 56 | self.restart_attempts_label = customtkinter.CTkLabel(self, text="Restart attempts", font=customtkinter.CTkFont(size=20, underline=True)) 57 | self.restart_attempts_label.grid(row=5, column=0, padx=20, pady=(20, 10)) 58 | self.restart_attempts_tootltip = CTkToolTip(self.restart_attempts_label, message="Sets the number of restart attempts allowed to the script. Restart attempts are triggered when the game crashes or freezes.", wraplength=400) 59 | 60 | self.restart_attempts_spinbox = CTkIntegerSpinbox(self, step_size=1, min_value=0, command=lambda x=["login", "restart_attempts"]:self.config.save_to_json(x)) 61 | self.restart_attempts_spinbox.entry.bind("", lambda event, x=["login", "restart_attempts"]: self.config.save_to_json(x)) 62 | self.restart_attempts_spinbox.grid(row=5, column=1, padx=20, pady=(20, 10)) 63 | 64 | self.linker.widgets["login"]["restart_attempts"] = self.restart_attempts_spinbox 65 | 66 | def create_autorun_widgets(self): 67 | self.core_label = customtkinter.CTkLabel(self, text="Startup", font=customtkinter.CTkFont(family="Inter", size=26, weight="bold")) 68 | self.core_label.grid(row=6, column=0, sticky="nw", padx=20, pady=20) 69 | 70 | self.autostart_checkbox = customtkinter.CTkCheckBox(self, text="Auto Start Script", font=customtkinter.CTkFont(size=20, weight="bold", underline=True), command=lambda x=["login", "auto_start"]: self.config.save_to_json(x)) 71 | self.rautostart_tootltip = CTkToolTip(self.autostart_checkbox, message="Script will automatically start after launching BAAuto.", wraplength=400) 72 | self.autostart_checkbox.grid(row=7, column=0, padx=40, pady=(20, 10), sticky="nw") 73 | self.linker.widgets["login"]["auto_start"] = self.autostart_checkbox 74 | 75 | def create_emulator_widgets(self): 76 | self.emulator_checkbox = customtkinter.CTkCheckBox(self, text="Launch Emulator", font=customtkinter.CTkFont(size=20, underline=True), command=lambda x=["login", "launch_emulator"]: self.config.save_to_json(x)) 77 | self.emulator_tooltip = CTkToolTip(self.emulator_checkbox, 78 | message="When enabled, the script will launch the emulator if it doesn't find any device with the specified address. Emulator path MUST be provided and be valid.", 79 | wraplength=400) 80 | 81 | self.emulator_checkbox.grid(row=8, column=0, padx=40, pady=(20, 10), sticky="nw") 82 | self.linker.widgets["login"]["launch_emulator"] = self.emulator_checkbox 83 | 84 | self.emulator_path_entry = customtkinter.CTkEntry(self, font=customtkinter.CTkFont(family="Inter", size=16)) 85 | self.emulator_path_entry.grid(row=9, column=0, columnspan=2, padx=(60,0), pady=(20, 10), sticky="nsew") 86 | self.emulator_path_entry.bind("", lambda event, x=["login", "emulator_path"]: (self.config.save_to_json(x))) 87 | self.linker.widgets["login"]["emulator_path"] = self.emulator_path_entry 88 | 89 | self.emulator_path_button = customtkinter.CTkButton(self, width=50, text="Select", command = self.open_file) 90 | self.emulator_path_button.grid(row=9, column=2, padx=20, pady=(20, 10), sticky="nsew") 91 | 92 | self.delay_label = customtkinter.CTkLabel(self, text="Delay time (s)", font=customtkinter.CTkFont(size=20, family="Inter", underline=True)) 93 | self.delay_tooltip = CTkToolTip(self.delay_label, message="The time for the script to wait after launching the emulator", wraplength=400) 94 | self.delay_label.grid(row=10, column=0, padx=20, pady=(20, 10)) 95 | 96 | self.delay_spinbox = CTkIntegerSpinbox(self, step_size=1, min_value=0, command=lambda x=["login", "delay"]:self.config.save_to_json(x)) 97 | self.delay_spinbox.entry.bind("", lambda event, x=["login", "delay"]: self.config.save_to_json(x)) 98 | self.linker.widgets["login"]["delay"] = self.delay_spinbox 99 | self.delay_spinbox.grid(row=10, column=1) 100 | 101 | 102 | def open_file(self): 103 | filepath = filedialog.askopenfilename(filetypes=[("Executable files", "*.exe;*.lnk")]) 104 | if filepath != "": 105 | self.emulator_path_entry.delete(0, END) 106 | self.emulator_path_entry.insert(0, filepath) 107 | self.config.save_to_json(["login", "emulator_path"]) 108 | -------------------------------------------------------------------------------- /gui/frames/sidebar.py: -------------------------------------------------------------------------------- 1 | import customtkinter 2 | from PIL import Image 3 | from gui.frames.login import LoginFrame 4 | from gui.frames.cafe import CafeFrame 5 | from gui.frames.farming import FarmingFrame 6 | from gui.frames.claim_rewards import ClaimRewardsFrame 7 | from gui.custom_widgets.ctk_tooltip import CTkToolTip 8 | 9 | class Sidebar(customtkinter.CTkFrame): 10 | def __init__(self, master, linker, config, **kwargs): 11 | self.master = master 12 | self.linker = linker 13 | self.config = config 14 | super().__init__(master=self.master, **kwargs) 15 | self.grid_rowconfigure((0, 1, 2), weight=1) 16 | self.grid_columnconfigure(0, weight=1) 17 | karin_logo = customtkinter.CTkImage(light_image=Image.open("gui/icons/karin.png"), size=(152,152)) 18 | karin_logo_label = customtkinter.CTkLabel(self, image=karin_logo, text="") 19 | karin_logo_label.grid(row=0, column=0, sticky="nsew") 20 | self.gear_on = customtkinter.CTkImage(Image.open("gui/icons/gear_on.png"), size=(50,38)) 21 | self.gear_off = customtkinter.CTkImage(Image.open("gui/icons/gear_off.png"), size=(50,38)) 22 | self.create_module_frames() 23 | self.create_all_button_frame() 24 | self.create_then_frame() 25 | self.create_start_button() 26 | self.create_notification_frames() 27 | self.linker.sidebar = self 28 | 29 | def create_module_frames(self): 30 | 31 | self.checkbox_frame = customtkinter.CTkFrame(self, fg_color="transparent", border_color="white", border_width=2) 32 | self.checkbox_frame.grid(row=1, column=0, columnspan=4, padx=10, pady=10, sticky="w") 33 | 34 | self.module_list = [["login", LoginFrame], ["cafe", CafeFrame], ["farming", FarmingFrame], ["claim_rewards", ClaimRewardsFrame]] 35 | for index, sublist in enumerate(self.module_list): 36 | module = sublist[0] 37 | self.linker.modules_dictionary[module] = {} 38 | self.create_module_checkbox(module, index) 39 | self.create_module_button(module, index) 40 | frame = sublist[1](self.master, self.linker, self.config, fg_color="#262250") 41 | self.linker.modules_dictionary[module]['frame'] = frame 42 | self.linker.modules_dictionary["login"]["button"].configure(image=self.gear_on) 43 | self.linker.modules_dictionary["login"]["checkbox"].configure(text_color="#53B9E9") 44 | self.current_frame = self.linker.modules_dictionary["login"]["frame"] # Update the current frame 45 | self.current_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") 46 | 47 | def create_module_checkbox(self, module, i): 48 | self.linker.modules_dictionary[module]['checkbox'] = customtkinter.CTkCheckBox( 49 | self.checkbox_frame, text=self.linker.capitalise(module), text_color="#FFFFFF", font=("Inter", 16), command=lambda x=[module, "enabled"]: self.config.save_to_json(x)) 50 | self.linker.modules_dictionary[module]['checkbox'].grid(row=i, column=0, columnspan=2,padx=20, pady=(10, 5), sticky="nw") 51 | self.linker.widgets[module]['enabled'] = self.linker.modules_dictionary[module]['checkbox'] 52 | 53 | def create_module_button(self, module, i): 54 | self.linker.modules_dictionary[module]['button'] = customtkinter.CTkButton( 55 | self.checkbox_frame, width=50, image=self.gear_off, text="", fg_color="transparent", command=lambda x=module: self.display_settings(module)) 56 | self.linker.modules_dictionary[module]['button'].grid(row=i, column=1, padx=(40,0), pady=(2,0), sticky="nw") 57 | 58 | def create_all_button_frame(self): 59 | self.select_all_button = customtkinter.CTkButton(self.checkbox_frame, width=100, text="Select All", fg_color="#DC621D", font=("Inter",20), command=self.select_all) 60 | self.select_all_button.grid(row=4, column=0, padx=10, pady=(15,20), sticky="w") 61 | self.clear_all_button = customtkinter.CTkButton(self.checkbox_frame, width=100, text="Clear All", fg_color="#DC621D", font=("Inter",20), command=self.clear_all) 62 | self.clear_all_button.grid(row=4, column=1, padx=10, pady=(15,20), sticky="w") 63 | 64 | def create_then_frame(self): 65 | self.then_frame = customtkinter.CTkFrame(self, fg_color="transparent") 66 | self.then_frame.grid(row=2, column=0) 67 | 68 | self.then_label = customtkinter.CTkLabel(self.then_frame, text="Then", font=customtkinter.CTkFont(size=16, family="Inter", underline=True)) 69 | self.then_label.grid(row=0, column=0, padx=(0, 10), sticky="nw") 70 | self.then_tooltip = CTkToolTip(self.then_label, 71 | message="Administrator privileges most likely required for exiting emulator and shutting down. For exiting emulator, path MUST be provided in Login Settings and be valid.", 72 | wraplength=400) 73 | 74 | then_values = ["Do Nothing", "Exit BAAuto", "Exit Emulator", "Exit BAAuto and Emulator", "Shutdown"] 75 | self.then_optionmenu = customtkinter.CTkOptionMenu(self.then_frame, values=then_values, command=lambda choice: self.save_then(choice)) 76 | self.then_optionmenu.grid(row=0, column=1, sticky="nw") 77 | 78 | 79 | self.linker.widgets["then"] = self.then_optionmenu 80 | 81 | def create_start_button(self): 82 | self.start_button = customtkinter.CTkButton(self, text="Start", width=200, height=40, command=self.linker.start_stop, font=customtkinter.CTkFont(family="Inter", size=16)) 83 | self.start_button.grid(row=3, column=0, pady=20, sticky="n") 84 | 85 | def create_notification_frames(self): 86 | for index, element in enumerate(["Template", "Queue", "Configuration"]): 87 | frame = customtkinter.CTkFrame(self, fg_color="transparent", height=50) 88 | if index == 0: 89 | top_pady=170 90 | else: 91 | top_pady=0 92 | frame.grid(row=3+index, column=0, sticky="s", pady=(top_pady,0)) 93 | self.linker.name_to_sidebar_frame[element] = frame 94 | 95 | def select_all(self): 96 | for module in self.linker.modules_dictionary: 97 | if module != "momotalk": 98 | self.linker.modules_dictionary[module]["checkbox"].select() 99 | self.config.config_data[module]["enabled"] = True 100 | self.config.save_file("Configuration") 101 | 102 | def clear_all(self): 103 | for module in self.linker.modules_dictionary: 104 | self.linker.modules_dictionary[module]["checkbox"].deselect() 105 | self.config.config_data[module]["enabled"] = False 106 | self.config.save_file("Configuration") 107 | 108 | def save_then(self, choice): 109 | self.config.config_data["then"] = choice 110 | self.config.save_file() 111 | 112 | def display_settings(self, module): 113 | for key in self.linker.modules_dictionary: 114 | if key == module: 115 | self.linker.modules_dictionary[key]["button"].configure(image=self.gear_on) 116 | self.linker.modules_dictionary[key]["checkbox"].configure(text_color="#53B9E9") 117 | self.current_frame.grid_remove() # Hide the current frame 118 | self.current_frame = self.linker.modules_dictionary[key]["frame"] # Update the current frame 119 | self.current_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew") 120 | else: 121 | self.linker.modules_dictionary[key]["button"].configure(image=self.gear_off) 122 | self.linker.modules_dictionary[key]["checkbox"].configure(text_color="#FFFFFF") 123 | -------------------------------------------------------------------------------- /gui/icons/gear_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/icons/gear_off.png -------------------------------------------------------------------------------- /gui/icons/gear_on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/icons/gear_on.png -------------------------------------------------------------------------------- /gui/icons/karin.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/icons/karin.ico -------------------------------------------------------------------------------- /gui/icons/karin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/gui/icons/karin.png -------------------------------------------------------------------------------- /gui/student_list/CN.json: -------------------------------------------------------------------------------- 1 | [ 2 | "\u4e43\u7231", 3 | "\u4e9a\u4f3d\u91cc", 4 | "\u4e9a\u5b50", 5 | "\u4f0a\u5415\u6ce2", 6 | "\u4f0a\u7ec7", 7 | "\u4f0a\u7ec7\uff08\u6cf3\u88c5\uff09", 8 | "\u4f18\u9999", 9 | "\u4f18\u9999\uff08\u4f53\u80b2\u670d\uff09", 10 | "\u4f73\u4ee3\u5b50", 11 | "\u5343\u590f", 12 | "\u5343\u590f\uff08\u6e29\u6cc9\uff09", 13 | "\u5343\u5bfb", 14 | "\u5343\u7eb1", 15 | "\u548f\u53f6", 16 | "\u548f\u53f6\uff08\u5e94\u63f4\u56e2\uff09", 17 | "\u54cd", 18 | "\u54cd\uff08\u5e94\u63f4\u56e2\uff09", 19 | "\u559c\u7f8e", 20 | "\u5807", 21 | "\u590f", 22 | "\u5b81\u7460", 23 | "\u5b81\u7460\uff08\u5154\u5973\u90ce\uff09", 24 | "\u5c0f\u7389", 25 | "\u5f25\u9999", 26 | "\u5f26\u751f", 27 | "\u5f26\u751f\uff08\u6cf3\u88c5\uff09", 28 | "\u5fd7\u7f8e\u5b50", 29 | "\u6167", 30 | "\u6566\u5b50", 31 | "\u65e0\u6708", 32 | "\u65e5\u548c", 33 | "\u65e5\u5bcc\u7f8e\uff08\u6cf3\u88c5\uff09", 34 | "\u65e5\u6b65\u7f8e", 35 | "\u660e\u65e5\u5948", 36 | "\u661f\u91ce", 37 | "\u661f\u91ce\uff08\u6cf3\u88c5\uff09", 38 | "\u6674", 39 | "\u6674\u5948\uff08\u6b63\u6708\uff09", 40 | "\u6731\u97f3", 41 | "\u6731\u97f3\uff08\u5154\u5973\u90ce\uff09", 42 | "\u6843\u4e95", 43 | "\u6893", 44 | "\u6893\uff08\u6cf3\u88c5\uff09", 45 | "\u6a31\u5b50", 46 | "\u6cc9", 47 | "\u6cc9\uff08\u6cf3\u88c5\uff09", 48 | "\u6e1a", 49 | "\u702c\u5948", 50 | "\u7231\u8389", 51 | "\u739b\u8389", 52 | "\u739b\u8389\uff08\u4f53\u64cd\u670d\uff09", 53 | "\u767d\u5b50", 54 | "\u771f\u7eaa", 55 | "\u7766\u6708\uff08\u6b63\u6708\uff09", 56 | "\u77e5\u4e16", 57 | "\u77e5\u4e16\uff08\u6cf3\u88c5\uff09", 58 | "\u7eaf\u5b50", 59 | "\u7eaf\u5b50\uff08\u6b63\u6708\uff09", 60 | "\u7eb1\u7ec7", 61 | "\u7eeb\u97f3", 62 | "\u7eeb\u97f3\uff08\u6cf3\u88c5\uff09", 63 | "\u7eff", 64 | "\u7f8e\u4f18", 65 | "\u7f8e\u54b2", 66 | "\u7f8e\u7962", 67 | "\u82b1\u51db", 68 | "\u82b1\u51db\uff08\u5154\u5973\u90ce\uff09", 69 | "\u82b1\u7ed8", 70 | "\u82b1\u7ed8\uff08\u5723\u8bde\uff09", 71 | "\u82b9\u5948", 72 | "\u82b9\u5948\uff08\u5723\u8bde\u8282\uff09", 73 | "\u82e5\u85fb", 74 | "\u82e5\u85fb\uff08\u6cf3\u88c5\uff09", 75 | "\u82f1\u7f8e", 76 | "\u831c\u9999", 77 | "\u831c\u9999\uff08\u6b63\u6708\uff09", 78 | "\u8331\u8389", 79 | "\u83b2\u5b9e", 80 | "\u83b2\u5b9e\uff08\u4f53\u80b2\u670d\uff09", 81 | "\u9065\u9999", 82 | "\u90fd\u5b50", 83 | "\u91ce\u4e43\u7f8e", 84 | "\u91ce\u4e43\u7f8e\uff08\u6cf3\u88c5\uff09", 85 | "\u94c3\u7f8e", 86 | "\u9633\u5948", 87 | "\u9633\u5948\uff08\u6cf3\u88c5\uff09", 88 | "\u9633\u8475", 89 | "\u963f\u9732", 90 | "\u963f\u9732\uff08\u6b63\u6708\uff09", 91 | "\u9ebb\u767d", 92 | "\u9ebb\u767d\uff08\u6cf3\u88c5\uff09" 93 | ] -------------------------------------------------------------------------------- /gui/student_list/CNscraper.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | import requests 3 | import json 4 | import concurrent.futures 5 | 6 | def generate_urls(): 7 | urls = [] 8 | html_file = requests.get('https://ba.gamekee.com/').text 9 | soup = BeautifulSoup(html_file, 'lxml') 10 | div = soup.find_all('div',class_ = "item-wrapper icon-size-6 pc-item-group") 11 | a = div[1].find_all('a') 12 | for link in a: 13 | urls.append(link.get('href')) 14 | return urls 15 | 16 | def find_student_name(url): 17 | r = requests.get('https://ba.gamekee.com' + url).text 18 | soup = BeautifulSoup(r, 'lxml') 19 | tables = soup.find_all('table', class_='mould-table selectItemTable col-group-table') 20 | for table in tables: 21 | for span in table.find_all('span'): 22 | if span.text == "学生信息": 23 | target_row = table.find_all('tr')[2] 24 | target_cell = target_row.find_all('td')[1] 25 | 26 | # Get the text inside the cell 27 | text = target_cell.get_text() 28 | 29 | # Print the text 30 | print('https://ba.gamekee.com' + url) 31 | print(text.strip()) 32 | return text.strip() 33 | 34 | 35 | def main(): 36 | urls = generate_urls() 37 | student_names = set() # Create an empty list to store the results 38 | with concurrent.futures.ThreadPoolExecutor() as executor: 39 | # Use submit to schedule tasks and collect Future objects 40 | futures = [executor.submit(find_student_name, url) for url in urls] 41 | 42 | # Use as_completed to iterate over completed futures 43 | for future in concurrent.futures.as_completed(futures): 44 | result = future.result() # Get the result from the completed future 45 | if result is None or any(char.isdigit() for char in result): 46 | print("Anomaly\n") 47 | 48 | elif result in student_names: 49 | student_names.remove(result) 50 | print("Anomaly\n") 51 | else: 52 | print("\n") 53 | student_names.add(result) # Append the result to the list 54 | 55 | # Now student_names contains all the collected student names 56 | student_names = list(student_names) 57 | student_names.sort() 58 | with open("CN.json", "w") as f: 59 | json.dump(student_names, f, indent=2) 60 | 61 | if __name__ == '__main__': 62 | main() 63 | -------------------------------------------------------------------------------- /gui/student_list/EN.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Airi", 3 | "Akane", 4 | "Akane (Bunny Girl)", 5 | "Akari", 6 | "Ako", 7 | "Arisu", 8 | "Arisu (Maid)", 9 | "Aru", 10 | "Aru (New Year)", 11 | "Asuna", 12 | "Asuna (Bunny Girl)", 13 | "Atsuko", 14 | "Ayane", 15 | "Ayane (Swimsuit)", 16 | "Azusa", 17 | "Azusa (Swimsuit)", 18 | "Cherino", 19 | "Cherino (Hot Spring)", 20 | "Chihiro", 21 | "Chinatsu", 22 | "Chinatsu (Hot Spring)", 23 | "Chise", 24 | "Chise (Swimsuit)", 25 | "Eimi", 26 | "Fubuki", 27 | "Fuuka", 28 | "Fuuka (New Year)", 29 | "Hanae", 30 | "Hanae (Christmas)", 31 | "Hanako", 32 | "Hanako (Swimsuit)", 33 | "Hare", 34 | "Haruka", 35 | "Haruka (New Year)", 36 | "Haruna", 37 | "Haruna (New Year)", 38 | "Haruna (Sportswear)", 39 | "Hasumi", 40 | "Hasumi (Sportswear)", 41 | "Hatsune Miku", 42 | "Hibiki", 43 | "Hibiki (Cheerleader)", 44 | "Hifumi", 45 | "Hifumi (Swimsuit)", 46 | "Himari", 47 | "Hina", 48 | "Hina (Swimsuit)", 49 | "Hinata", 50 | "Hinata (Swimsuit)", 51 | "Hiyori", 52 | "Hoshino", 53 | "Hoshino (Swimsuit)", 54 | "Iori", 55 | "Iori (Swimsuit)", 56 | "Iroha", 57 | "Izumi", 58 | "Izumi (Swimsuit)", 59 | "Izuna", 60 | "Izuna (Swimsuit)", 61 | "Junko", 62 | "Junko (New Year)", 63 | "Juri", 64 | "Kaede", 65 | "Kaho", 66 | "Kanna", 67 | "Karin", 68 | "Karin (Bunny Girl)", 69 | "Kayoko", 70 | "Kayoko (New Year)", 71 | "Kazusa", 72 | "Kirino", 73 | "Koharu", 74 | "Koharu (Swimsuit)", 75 | "Kokona", 76 | "Kotama", 77 | "Kotori", 78 | "Kotori (Cheerleader)", 79 | "Koyuki", 80 | "Maki", 81 | "Mari", 82 | "Mari (Sportswear)", 83 | "Marina", 84 | "Mashiro", 85 | "Mashiro (Swimsuit)", 86 | "Megu", 87 | "Meru", 88 | "Michiru", 89 | "Midori", 90 | "Mika", 91 | "Mimori", 92 | "Mimori (Swimsuit)", 93 | "Mina", 94 | "Mine", 95 | "Minori", 96 | "Misaki", 97 | "Miyako", 98 | "Miyako (Swimsuit)", 99 | "Miyu", 100 | "Miyu (Swimsuit)", 101 | "Moe", 102 | "Momiji", 103 | "Momoi", 104 | "Mutsuki", 105 | "Mutsuki (New Year)", 106 | "Nagisa", 107 | "Natsu", 108 | "Neru", 109 | "Neru (Bunny Girl)", 110 | "Noa", 111 | "Nodoka", 112 | "Nodoka (Hot Spring)", 113 | "Nonomi", 114 | "Nonomi (Swimsuit)", 115 | "Pina", 116 | "Reisa", 117 | "Rumi", 118 | "Saki", 119 | "Saki (Swimsuit)", 120 | "Sakurako", 121 | "Saori", 122 | "Saya", 123 | "Saya (Casual)", 124 | "Sena", 125 | "Serika", 126 | "Serika (New Year)", 127 | "Serina", 128 | "Serina (Christmas)", 129 | "Shigure", 130 | "Shimiko", 131 | "Shiroko", 132 | "Shiroko (Riding)", 133 | "Shiroko (Swimsuit)", 134 | "Shizuko", 135 | "Shizuko (Swimsuit)", 136 | "Shun", 137 | "Shun (Kid)", 138 | "Sumire", 139 | "Suzumi", 140 | "Toki", 141 | "Toki (Bunny Girl)", 142 | "Tomoe", 143 | "Tsubaki", 144 | "Tsukuyo", 145 | "Tsurugi", 146 | "Tsurugi (Swimsuit)", 147 | "Ui", 148 | "Ui (Swimsuit)", 149 | "Utaha", 150 | "Utaha (Cheerleader)", 151 | "Wakamo", 152 | "Wakamo (Swimsuit)", 153 | "Yoshimi", 154 | "Yuuka", 155 | "Yuuka (Sportswear)", 156 | "Yuzu", 157 | "Yuzu (Maid)" 158 | ] -------------------------------------------------------------------------------- /licenses/ALAuto-license: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2019 Egoistically <298qjm982374@protonmail.com> 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. 14 | -------------------------------------------------------------------------------- /licenses/CTkMessagebox-license: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /licenses/CTkScrollableDropdown-license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Akash Bora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /licenses/CTkToolTip-license: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /licenses/ascreencap-license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 NewView 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /modules/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/modules/__init__.py -------------------------------------------------------------------------------- /modules/bounty.py: -------------------------------------------------------------------------------- 1 | from util.logger import Logger 2 | from util.utils import GoTo, Region, Utils 3 | 4 | 5 | class BountyModule(object): 6 | def __init__(self, config): 7 | """Initializes the Bounty module. 8 | 9 | Args: 10 | config (Config): BAAuto Config instance 11 | """ 12 | self.enabled = True 13 | self.config = config 14 | 15 | # Define the stages and their corresponding configuration settings 16 | self.stage = { 17 | 'overpass': self.config.bounty['overpass'], 18 | 'desert_railroad': self.config.bounty['desert_railroad'], 19 | 'classroom': self.config.bounty['classroom'] 20 | } 21 | 22 | # Define regions for various elements on the screen 23 | self.region = { 24 | 'tickets': None, 25 | 'overpass': (800,200), 26 | 'desert_railroad': (800,310), 27 | 'classroom': (800, 410), 28 | 'stage_list': Region(677, 132, 747, 678) 29 | } 30 | 31 | def determine_tickets_region(self): 32 | if Utils.assets == 'EN': 33 | self.region['tickets'] = Region(225, 75, 45, 45) 34 | elif Utils.assets == 'CN': 35 | self.region['tickets'] = Region(155,80, 50, 40) 36 | 37 | def bounty_logic_wrapper(self): 38 | # Navigate to the Bounty campaign 39 | GoTo.sub_campaign('bounty') 40 | 41 | # Read available locations from tickets and create a queue 42 | locations_queue = {k:v for k,v in self.stage.items() if v["run_times"] != 0} 43 | if locations_queue == {}: 44 | Logger.log_warning = "Bounty was enabled but all locations run times were set to 0. Unable to proceed." 45 | return 46 | self.determine_tickets_region() 47 | if Utils.scan(self.region["tickets"],resize=True)[0]["text"].strip() == "0/6": 48 | Logger.log_warning("Not enough tickets to run bounty.") 49 | return 50 | # Loop through the locations queue 51 | for entry in locations_queue: 52 | while Utils.find('goto/bounty'): 53 | Utils.touch(*self.region[entry]) 54 | Utils.update_screen() 55 | 56 | # Find the region for the current stage 57 | stage_region = Utils.find_stage(self.stage[entry]["stage"]) 58 | 59 | if stage_region: 60 | # Find and touch the "Enter" button for the stage 61 | button = Utils.find_button('farming/small_enter', stage_region, self.region['stage_list']) 62 | if button: 63 | while True: 64 | Utils.wait_update_screen(1) 65 | if not Utils.find('farming/sweep'): 66 | Utils.touch_randomly(button) 67 | continue 68 | outcome = Utils.sweep(locations_queue[entry]["run_times"]) # Perform the sweep action 69 | if outcome[0] == "incomplete": 70 | if outcome[1] == 0: 71 | Logger.log_warning("Not enough tickets to complete sweep") 72 | else: 73 | Logger.log_warning(f'Ran out of tickets but enough to complete stage {outcome[1]} times instead of {locations_queue[entry]["run_times"]}') 74 | return 75 | break 76 | else: 77 | Logger.log_error(f'{entry.capitalize()}-{self.stage[entry]["stage"]} not unlocked') 78 | while not Utils.find("goto/bounty", color=True): 79 | Utils.touch(55,40) 80 | Utils.wait_update_screen(1) 81 | -------------------------------------------------------------------------------- /modules/cafe.py: -------------------------------------------------------------------------------- 1 | from util.logger import Logger 2 | from util.utils import Utils, Region, GoTo 3 | from tqdm import tqdm 4 | 5 | class CafeModule(object): 6 | def __init__(self, config): 7 | """Initializes the Cafe module. 8 | 9 | Args: 10 | config (Config): BAAuto Config instance 11 | """ 12 | self.enabled = True 13 | self.config = config 14 | 15 | # Define regions for various elements on the screen 16 | self.region = { 17 | 'invite': Region(800, 620, 55, 70), 18 | 'momotalk': Region(410, 187, 290, 413), 19 | } 20 | 21 | def cafe_logic_wrapper(self): 22 | # Navigate to the Cafe screen 23 | GoTo.sub_home('cafe') 24 | Utils.update_screen() 25 | 26 | # Invite a student if the configuration allows and a student is available 27 | if self.config.cafe['invite_student'] and Utils.find("cafe/available"): 28 | self.find_student() 29 | 30 | # Tap on students if the configuration allows 31 | if self.config.cafe['tap_students']: 32 | # Define ranges for tapping students in a grid 33 | y_range = range(140, 543, 50) 34 | x_range = range(0, 1365, 50) 35 | total_iterations = len(y_range) * len(x_range) 36 | 37 | # Create a progress bar for tapping students 38 | progress_bar = tqdm(total=total_iterations, unit="taps", colour='cyan', desc='Tapping Students') 39 | 40 | # Loop through the grid and tap on students 41 | for y in y_range: 42 | for x in x_range: 43 | Utils.touch(x, y) 44 | progress_bar.update(1) # Increment the progress bar 45 | 46 | # Claim earnings if the configuration allows 47 | if self.config.cafe['claim_earnings']: 48 | while True: 49 | Utils.wait_update_screen(1) 50 | if not Utils.find("cafe/earnings"): 51 | Utils.touch(1158, 647) # Touch the earnings button 52 | else: 53 | Utils.touch(640, 520) # Touch the claim button 54 | break 55 | 56 | # Return to the home screen 57 | GoTo.home() 58 | 59 | def find_student(self): 60 | found = False 61 | last_student = "" 62 | if self.config.cafe["student_name"].replace(" ", "") == "": 63 | Logger.log_warning("Inviting student is turned on but student name is empty. Unable to proceed.") 64 | return 65 | Logger.log_info(f'Inviting student: {self.config.cafe["student_name"]}') 66 | 67 | # Wait for the momotalk screen to appear 68 | while not Utils.find("cafe/momotalk"): 69 | Utils.touch_randomly(self.region['invite']) 70 | Utils.wait_update_screen(1) 71 | 72 | # Continue searching for the student until found or no more students are available 73 | Utils.init_ocr_mode() 74 | while not found: 75 | Utils.wait_update_screen(1) 76 | 77 | # Search for the student's name in the momotalk region 78 | result = Utils.find_word(self.config.cafe['student_name'], self.region['momotalk']) 79 | 80 | if result[0]: 81 | Logger.log_success("Student found!") 82 | found = True 83 | 84 | # Find and touch the invite button for the student 85 | button = Utils.find_button('cafe/invite', result[1], self.region['momotalk']) 86 | 87 | if button: 88 | while True: 89 | Utils.wait_update_screen(1) 90 | 91 | # Confirm the invitation 92 | if Utils.find_and_touch('cafe/confirm'): 93 | break 94 | Utils.touch_randomly(button) 95 | else: 96 | Logger.log_error('Error: Could not find button') 97 | elif last_student == result[1]: 98 | Logger.log_error('Student not found. Please check spelling.') 99 | break 100 | else: 101 | last_student = result[1] if not result[1].isdigit() else last_student 102 | Utils.swipe(600, 500, 600, 200) # Swipe to scroll the momotalk region 103 | Utils.init_ocr_mode(EN=True) 104 | -------------------------------------------------------------------------------- /modules/claim_rewards.py: -------------------------------------------------------------------------------- 1 | from util.logger import Logger 2 | from util.utils import Utils, GoTo 3 | 4 | class ClaimRewardsModule(object): 5 | def __init__(self, config): 6 | """Initializes the Claim Rewards module. 7 | 8 | Args: 9 | config (Config): BAAuto Config instance 10 | """ 11 | self.enabled = True 12 | self.config = config 13 | 14 | def claim_rewards_logic_wrapper(self): 15 | if self.config.claim_rewards['club']: 16 | GoTo.sub_home('club') 17 | # Check if there are tasks to claim rewards from 18 | if self.config.claim_rewards['tasks']: 19 | # Navigate to the tasks section 20 | GoTo.sub_home('tasks') 21 | 22 | while True: 23 | Utils.update_screen() 24 | 25 | # Check if the "Claim All" button is available in tasks 26 | if Utils.find('claim_rewards/tasks_claim_all', color=True): 27 | Utils.touch(1150, 670) # Touch the "Claim All" button 28 | GoTo.sub_home('tasks') # Return to the tasks section 29 | continue 30 | 31 | # Check if individual task rewards can be claimed 32 | if Utils.find('claim_rewards/claim', color=True): 33 | Utils.touch(970, 675) # Touch the individual task reward 34 | GoTo.sub_home('tasks') # Return to the tasks section 35 | continue 36 | 37 | break # Exit the loop if no more rewards can be claimed in tasks 38 | 39 | # Check if there are rewards to claim from the mailbox 40 | if self.config.claim_rewards['mailbox']: 41 | # Navigate to the club and then the mailbox 42 | GoTo.sub_home('mailbox') 43 | 44 | while True: 45 | Utils.update_screen() 46 | 47 | # Check if the "Claim All" button is available in the mailbox 48 | if Utils.find('claim_rewards/mailbox_claim_all', color=True): 49 | Utils.touch(1150, 670) # Touch the "Claim All" button 50 | GoTo.sub_home('mailbox') # Return to the mailbox 51 | continue 52 | 53 | break # Exit the loop if no more rewards can be claimed in the mailbox 54 | 55 | # Return to the home screen and reset reward claim flags 56 | GoTo.home() -------------------------------------------------------------------------------- /modules/login.py: -------------------------------------------------------------------------------- 1 | from util.utils import Region, Utils 2 | from util.logger import Logger 3 | from util.config import Config 4 | from util.utils import Utils, GoTo 5 | import time 6 | 7 | class LoginModule(object): 8 | 9 | def __init__(self, config): 10 | """Initializes the Login module. 11 | 12 | Args: 13 | config (Config): BAAuto Config instance 14 | """ 15 | self.enabled = True 16 | self.config = config 17 | 18 | def login_logic_wrapper(self): 19 | GoTo.home() 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /modules/mission.py: -------------------------------------------------------------------------------- 1 | from util.logger import Logger 2 | from util.config import Config 3 | from util.utils import Utils, Region, GoTo 4 | from datetime import datetime, time 5 | import json 6 | import copy 7 | import os 8 | 9 | class MissionModule(object): 10 | def __init__(self, config): 11 | """Initializes the Mission Module. 12 | 13 | Args: 14 | config (Config): BAAuto Config instance 15 | """ 16 | self.enabled = True 17 | self.config = config 18 | self.acronym = { 19 | 'N': 'Mission Normal', 20 | 'H': 'Mission Hard', 21 | 'BR': 'Commissions Base Defense', 22 | 'IR': 'Commissions Item Retrieval', 23 | 'E': "Event" 24 | } 25 | self.region = { 26 | 'ap': Region(555, 0, 105, 45), 27 | 'stage_list': Region(677, 132, 747, 678) 28 | } 29 | 30 | def mission_logic_wrapper(self): 31 | clear_queue = False 32 | recharged = False 33 | location = None 34 | current_ap, max_ap = self.read_ap() 35 | Logger.log_info(f'AP detected: {current_ap}/{max_ap}') 36 | 37 | # Check if there's enough AP to proceed 38 | if current_ap < 10: 39 | Logger.log_warning('Not enough AP to complete stages.') 40 | if self.config.mission['recharge_ap']: 41 | Logger.log_info('Attempting to recharge AP...') 42 | self.recharge_ap() 43 | current_ap, max_ap = self.read_ap() 44 | if current_ap < 10: 45 | Logger.log_warning('Still not enough AP to complete stages. Unable to proceed.') 46 | Logger.log_info('Recharge AP disabled. Unable to complete stages.') 47 | return 48 | 49 | # Check if it's time to clear the queue or reset daily 50 | if self.config.mission['reset_daily']: 51 | clear_queue = self.check_reset_time() 52 | 53 | # If the queue is empty or needs clearing, populate it 54 | if clear_queue or self.config.mission['queue'] == []: 55 | preferred_template = self.config.mission['preferred_template'] 56 | self.config.mission['queue'] = copy.deepcopy(self.config.mission["templates"][preferred_template]) 57 | 58 | # Process each entry in the queue 59 | while self.config.mission['queue'] != []: 60 | entry = self.config.mission['queue'][0] 61 | if entry[0].upper() in ['N', 'H']: 62 | location = 'mission' 63 | sweep_status = self.mission(entry) 64 | elif entry[0].upper() in ['BD', 'IR']: 65 | location = 'commissions' 66 | sweep_status = self.commissions(entry) 67 | else: 68 | location = 'event' 69 | sweep_status = self.event(entry) 70 | 71 | # Handle incomplete sweeps 72 | if sweep_status[0] == 'incomplete': 73 | current_ap, max_ap = self.read_ap(location) 74 | if not recharged and self.config.mission['recharge_ap']: 75 | if current_ap < 20: 76 | Logger.log_info('Attempting to recharge AP...') 77 | self.config.mission['queue'][0][2] -= sweep_status[1] 78 | self.update_config() 79 | self.recharge_ap() 80 | recharged = True 81 | continue 82 | else: 83 | self.config.mission['queue'][0][2] -= sweep_status[1] 84 | self.update_config() 85 | Logger.log_info(f'AP left: {current_ap} / {max_ap}. Unable to complete stages.') 86 | return 87 | 88 | self.config.mission['queue'].pop(0) 89 | self.update_config() 90 | 91 | def mission(self, entry): 92 | # Logic for mission stages 93 | GoTo.sub_campaign('mission') 94 | mode, stage, run_times = entry 95 | Logger.log_info(f'Current stage: {self.acronym[mode]} {stage}, run {run_times} times') 96 | area = int(stage.split('-')[0].strip()) 97 | 98 | if not self.find_area(area): 99 | Logger.log_error('Area not found, please check spelling. Skipping stage...') 100 | return ('failed') 101 | 102 | if mode.upper() == 'H': 103 | if run_times > 3: 104 | Logger.log_warning(f'Hard {stage} was set to be swept {run_times}. Reset to 3 as it surpass limit.') 105 | run_times = 3 106 | 107 | while Utils.find('farming/normal'): 108 | Utils.touch(1065, 160) 109 | Utils.wait_update_screen(1) 110 | else: 111 | while not Utils.find('farming/normal'): 112 | Utils.touch(915, 160) 113 | Utils.wait_update_screen(1) 114 | 115 | return self.attempt_stage(mode, stage, run_times) 116 | 117 | def commissions(self, entry): 118 | # Logic for commission stages 119 | GoTo.sub_campaign('commissions') 120 | mode, stage, run_times = entry 121 | 122 | while Utils.find('goto/commissions'): 123 | if mode.upper() == 'BD': 124 | Utils.touch(800, 200) 125 | else: 126 | Utils.touch(800, 310) 127 | Utils.update_screen() 128 | 129 | return self.attempt_stage(mode, stage, run_times) 130 | 131 | def event(self, entry): 132 | if self.config.mission["event"] == True: 133 | event_banner_path = f"assets/{Utils.assets}/goto/event_banner.png" 134 | mode, stage, run_times = entry 135 | Logger.log_info(f'Current stage: {self.acronym[mode]} {stage}, run {run_times} times') 136 | if os.path.exists(event_banner_path): 137 | GoTo.event() 138 | while Utils.find_and_touch('farming/quest'): 139 | Utils.wait_update_screen(1) 140 | return self.attempt_stage(mode, stage, run_times) 141 | else: 142 | Logger.log_warning("Event Banner not found. Unable to run event stages.") 143 | return ("failed") 144 | 145 | 146 | def find_area(self, desired_area): 147 | # Find and navigate to the desired area 148 | roi = Region(108, 178, 62, 37) 149 | left = (45, 360) 150 | right = (1240, 360) 151 | last_area = None 152 | 153 | while True: 154 | Utils.wait_update_screen(1) 155 | detected_area = int(Utils.scan(roi, resize=True)[0]['text']) 156 | 157 | if detected_area == last_area: 158 | return False 159 | elif detected_area < desired_area: 160 | Utils.touch(*right) 161 | last_area = detected_area 162 | elif detected_area > desired_area: 163 | Utils.touch(*left) 164 | last_area = detected_area 165 | else: 166 | return True 167 | 168 | def attempt_stage(self, mode, stage, run_times): 169 | # Logic for attempting a stage 170 | stage_region = Utils.find_stage(stage) 171 | 172 | if stage_region: 173 | if mode in ['H', 'E']: 174 | button = Utils.find_button('farming/big_enter', stage_region, self.region['stage_list']) 175 | else: 176 | button = Utils.find_button('farming/small_enter', stage_region, self.region['stage_list']) 177 | 178 | if button: 179 | while True: 180 | Utils.wait_update_screen(1) 181 | if not Utils.find('farming/sweep'): 182 | Utils.touch_randomly(button) 183 | continue 184 | outcome = Utils.sweep(run_times) 185 | if outcome[0] == "incomplete" and outcome[1] != 0: 186 | Logger.log_warning(f'Ran out of AP but enough to complete stage {outcome[1]} times instead of {run_times}') 187 | return outcome 188 | else: 189 | Logger.log_error(f'{self.acronym[mode]} {stage} is not unlocked') 190 | return ('failed') 191 | 192 | def read_ap(self, location=None): 193 | # Read and return current AP and max AP 194 | if location is None: 195 | GoTo.sub_home('campaign') 196 | elif location == "event": 197 | GoTo.event() 198 | else: 199 | GoTo.sub_campaign(location) 200 | waiting_time = 0 201 | ap = [""] 202 | # solution to AP being hidden by tasks notifications 203 | while waiting_time <= 5 and not ap[0].isdigit(): 204 | Utils.wait_update_screen(1) 205 | ap = Utils.scan(self.region['ap'])[0]['text'] 206 | waiting_time += 1 207 | if not ap[0].isdigit(): 208 | Logger.log_error("Error reading AP") 209 | return [int(x) for x in [x.strip() for x in ap.split('/')]] 210 | 211 | def recharge_ap(self): 212 | # Recharge AP if configured to do so 213 | if self.config.mission['recharge_ap']: 214 | if self.config.cafe['enabled'] and self.config.cafe['claim_earnings']: 215 | GoTo.sub_home('cafe') 216 | while True: 217 | Utils.wait_update_screen(1) 218 | if not Utils.find("cafe/earnings"): 219 | Utils.touch(1158, 647) 220 | else: 221 | Utils.touch(640, 520) 222 | break 223 | if self.config.claim_rewards['enabled']: 224 | from modules.claim_rewards import ClaimRewardsModule 225 | ClaimRewardsModule(self.config).claim_rewards_logic_wrapper() 226 | 227 | def check_reset_time(self): 228 | # Check if it's time to reset the queue 229 | current_datetime = datetime.now().replace(microsecond=0) # Round to the nearest second 230 | current_date = current_datetime.date() 231 | current_time = current_datetime.time() 232 | last_run_datetime = datetime.strptime(self.config.mission["last_run"], "%Y-%m-%d %H:%M:%S") 233 | reset_time = datetime.strptime(self.config.mission["reset_time"], "%H:%M:%S").time() 234 | 235 | if current_date != last_run_datetime.date() and current_time >= reset_time: 236 | self.update_config(last_run=True) 237 | Logger.log_info("Reset Daily activated. Resetting queue...") 238 | return True 239 | return False 240 | 241 | def update_config(self, last_run=False): 242 | # Update the configuration file with the current queue and last run time 243 | with open('config.json', 'r') as json_file: 244 | config_data = json.load(json_file) 245 | config_data["farming"]['mission']['queue'] = self.config.mission['queue'] 246 | if last_run: 247 | config_data["farming"]['mission']["last_run"] = str(datetime.now().replace(microsecond=0)) 248 | with open("config.json", "w") as json_file: 249 | json.dump(config_data, json_file, indent=2) 250 | -------------------------------------------------------------------------------- /modules/scrimmage.py: -------------------------------------------------------------------------------- 1 | from util.logger import Logger 2 | from util.utils import GoTo, Region, Utils 3 | 4 | 5 | class ScrimmageModule(object): 6 | def __init__(self, config): 7 | """Initializes the Bounty module. 8 | 9 | Args: 10 | config (Config): BAAuto Config instance 11 | """ 12 | self.enabled = True 13 | self.config = config 14 | 15 | # Define the stages and their corresponding configuration settings 16 | self.stage = { 17 | 'trinity': self.config.scrimmage['trinity'], 18 | 'gehenna': self.config.scrimmage['gehenna'], 19 | 'millennium': self.config.scrimmage['millennium'] 20 | } 21 | 22 | # Define regions for various elements on the screen 23 | self.region = { 24 | 'tickets': None, 25 | 'trinity': (800,200), 26 | 'gehenna': (800,310), 27 | 'millennium': (800, 410), 28 | 'stage_list': Region(677, 132, 747, 678) 29 | } 30 | 31 | def determine_tickets_region(self): 32 | if Utils.assets == 'EN': 33 | self.region['tickets'] = Region(225, 75, 45, 45) 34 | elif Utils.assets == 'CN': 35 | self.region['tickets'] = Region(155,80, 50, 40) 36 | 37 | def scrimmage_logic_wrapper(self): 38 | # Navigate to the Bounty campaign 39 | GoTo.sub_campaign('scrimmage') 40 | 41 | # Read available locations from tickets and create a queue 42 | locations_queue = {k:v for k,v in self.stage.items() if v["run_times"] != 0} 43 | if locations_queue == {}: 44 | Logger.log_warning = "Scrimmage was enabled but all locations run times were set to 0. Unable to proceed." 45 | return 46 | self.determine_tickets_region() 47 | if Utils.scan(self.region["tickets"],resize=True)[0]["text"].strip() == "0/6": 48 | Logger.log_warning("Not enough tickets to run bounty.") 49 | return 50 | # Loop through the locations queue 51 | for entry in locations_queue: 52 | while Utils.find('goto/scrimmage'): 53 | Utils.touch(*self.region[entry]) 54 | Utils.update_screen() 55 | 56 | # Find the region for the current stage 57 | stage_region = Utils.find_stage(self.stage[entry]["stage"]) 58 | 59 | if stage_region: 60 | # Find and touch the "Enter" button for the stage 61 | button = Utils.find_button('farming/small_enter', stage_region, self.region['stage_list']) 62 | if button: 63 | while True: 64 | Utils.wait_update_screen(1) 65 | if not Utils.find('farming/sweep'): 66 | Utils.touch_randomly(button) 67 | continue 68 | outcome = Utils.sweep(locations_queue[entry]["run_times"]) # Perform the sweep action 69 | if outcome[0] == "incomplete": 70 | if outcome[1] == 0: 71 | Logger.log_warning("Not enough tickets to complete sweep") 72 | else: 73 | Logger.log_warning(f'Ran out of tickets but enough to complete stage {outcome[1]} times instead of {locations_queue[entry]["run_times"]}') 74 | return 75 | break 76 | else: 77 | Logger.log_error(f'{entry.capitalize()}-{self.stage[entry]["stage"]} not unlocked') 78 | while not Utils.find("goto/scrimmage", color=True): 79 | Utils.touch(55,40) 80 | Utils.wait_update_screen(1) 81 | -------------------------------------------------------------------------------- /modules/tactical_challenge.py: -------------------------------------------------------------------------------- 1 | from util.logger import Logger 2 | from util.utils import Utils, Region, GoTo 3 | from tqdm import tqdm 4 | import re 5 | 6 | class TacticalChallengeModule(object): 7 | def __init__(self, config): 8 | """Initializes the Tactical Challenge module. 9 | 10 | Args: 11 | config (Config): BAAuto Config instance 12 | """ 13 | self.enabled = True 14 | self.config = config 15 | self.rank = self.config.tactical_challenge['rank'] 16 | self.wins = 0 17 | self.loses = 0 18 | 19 | # Define regions for various elements on the screen 20 | self.region = { 21 | 'tickets' : None, 22 | 'rival_1': Region(615, 190, 150, 60), 23 | 'rival_2': Region(615, 345, 150, 60), 24 | 'rival_3': Region(615, 505, 150, 60), 25 | 'outcome': Region(405, 240, 465, 210) 26 | } 27 | 28 | def determine_tickets_region(self): 29 | if Utils.assets == 'EN': 30 | self.region['tickets'] = Region(208, 472, 44, 33) 31 | elif Utils.assets == 'CN': 32 | self.region['tickets'] = Region(156,470, 54, 45) 33 | 34 | def tactical_challenge_logic_wrapper(self): 35 | # Go to the tactical challenge sub-campaign 36 | GoTo.sub_campaign('tactical_challenge') 37 | self.claim() 38 | self.determine_tickets_region() 39 | ticket_owned = self.read_tickets() 40 | 41 | while ticket_owned: 42 | match = self.find_match() 43 | 44 | while match: 45 | Utils.wait_update_screen() 46 | 47 | # Check if the formation screen is displayed 48 | if Utils.find_and_touch('tactical_challenge/formation'): 49 | continue 50 | 51 | # Check if there are no tickets available 52 | if Utils.find_and_touch('tactical_challenge/no_tick'): 53 | continue 54 | 55 | # Check if the mobilize button is available 56 | if Utils.find('tactical_challenge/mobilise'): 57 | self.read_outcome() 58 | ticket_owned = self.read_tickets() 59 | 60 | if not ticket_owned: 61 | return 62 | 63 | self.progress_bar(45) 64 | break 65 | 66 | # Check if there are no tickets left 67 | if Utils.find('tactical_challenge/no_tickets'): 68 | Logger.log_warning(f'Run out of tickets') 69 | return 70 | 71 | def claim(self): 72 | # Attempt to claim rewards 73 | for i in range(2): 74 | while True: 75 | Utils.update_screen() 76 | if not Utils.find_and_touch(f'tactical_challenge/claim/{i}', color=True): 77 | GoTo.sub_campaign('tactical_challenge') 78 | break 79 | 80 | def read_tickets(self): 81 | # Read the number of tickets owned 82 | Utils.update_screen() 83 | tickets_owned = Utils.scan(self.region['tickets'])[0]['text'] 84 | tickets_owned = tickets_owned.strip(' ') 85 | Logger.log_msg(f'Tickets owned: {tickets_owned}') 86 | 87 | if tickets_owned == '0/5': 88 | Logger.log_warning(f'Run out of tickets') 89 | Logger.log_info(f'Total Wins: {self.wins}') 90 | Logger.log_info(f'Total Loses: {self.loses}') 91 | return False 92 | 93 | return tickets_owned 94 | 95 | def find_match(self): 96 | # Find and select a match based on rank preference 97 | Utils.touch(1160, 145) 98 | Utils.wait_update_screen(2) 99 | extract_integer = lambda input_string: int(re.search(r'\d+', input_string).group()) if re.search(r'\d+', input_string) else None 100 | options = [ 101 | [self.region['rival_1'], extract_integer(Utils.scan(self.region['rival_1'])[0]['text'])], 102 | [self.region['rival_2'], extract_integer(Utils.scan(self.region['rival_2'])[0]['text'])], 103 | [self.region['rival_3'], extract_integer(Utils.scan(self.region['rival_3'])[0]['text'])] 104 | ] 105 | Logger.log_info(f'Ranks detected: {options[0][1]}, {options[1][1]}, {options[2][1]}') 106 | options.sort(key=lambda x: x[1], reverse=True) 107 | 108 | if self.rank.lower() == "highest": 109 | match = options[2] 110 | Logger.log_info(f'Ranks set to highest -> {match[1]}') 111 | elif self.rank.lower() == "middle": 112 | match = options[1] 113 | Logger.log_info(f'Ranks set to middle -> {match[1]}') 114 | else: 115 | match = options[0] 116 | Logger.log_info(f'Ranks set to lowest -> {match[1]}') 117 | 118 | Utils.touch_randomly(match[0]) 119 | Utils.wait_update_screen(2) 120 | return match 121 | 122 | def progress_bar(self, total): 123 | # Display a progress bar while waiting 124 | with tqdm(total=total, ncols=100, bar_format='{l_bar}{bar}{n_fmt}/{total_fmt}s', desc='Waiting Standby Time', colour='cyan') as pbar: 125 | for i in range(total): 126 | pbar.update(1) 127 | Utils.script_sleep(1) 128 | 129 | def read_outcome(self): 130 | while True: 131 | Utils.update_screen() 132 | 133 | # Check if the mobilize button is still available 134 | if Utils.find_and_touch('tactical_challenge/mobilise'): 135 | continue 136 | # Check if the battle result screen is displayed 137 | if Utils.find('tactical_challenge/battle_result'): 138 | outcome = Utils.find_word('lose', self.region['outcome']) 139 | 140 | if outcome[0]: 141 | Logger.log_msg('Result Battle: Lose') 142 | self.loses += 1 143 | else: 144 | Logger.log_msg('Result Battle: Win') 145 | self.wins += 1 146 | 147 | GoTo.sub_campaign('tactical_challenge') 148 | break 149 | 150 | while True: 151 | # Check if the "New Best Season" message is displayed 152 | if Utils.find('tactical_challenge/best'): 153 | Logger.log_success('New Best Season Scored!') 154 | 155 | GoTo.sub_campaign('tactical_challenge') 156 | break 157 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | imutils 2 | numpy 3 | opencv-python 4 | scipy 5 | lz4 6 | tqdm 7 | customtkinter 8 | uiautomator2 9 | pponnxcr 10 | Pillow 11 | psutil 12 | pywin32 -------------------------------------------------------------------------------- /script.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | import traceback 4 | import argparse 5 | import os 6 | import subprocess 7 | import time 8 | 9 | try: 10 | from modules.login import LoginModule 11 | from modules.cafe import CafeModule 12 | from modules.bounty import BountyModule 13 | from modules.scrimmage import ScrimmageModule 14 | from modules.mission import MissionModule 15 | from modules.tactical_challenge import TacticalChallengeModule 16 | from modules.claim_rewards import ClaimRewardsModule 17 | from util.adb import Adb 18 | from util.config import Config 19 | from util.logger import Logger 20 | from util.utils import Utils 21 | from util.exceptions import GameStuckError, GameNotRunningError, ReadOCRError 22 | class BAAuto(object): 23 | modules = { 24 | 'login': None, 25 | 'cafe': None, 26 | 'club': None, 27 | 'mission': None, 28 | 'bounty': None, 29 | 'scrimmage': None, 30 | 'tactical_challenge': None, 31 | 'claim_rewards': None, 32 | } 33 | 34 | def __init__(self, config): 35 | """Initializes the primary azurlane-auto instance with the passed in 36 | Config instance; 37 | 38 | Args: 39 | config (Config): BAAuto Config instance 40 | """ 41 | self.config = config 42 | if self.config.login['enabled']: 43 | self.modules['login'] = LoginModule(self.config) 44 | if self.config.cafe['enabled']: 45 | self.modules['cafe'] = CafeModule(self.config) 46 | if self.config.farming['enabled'] and self.config.bounty['enabled']: 47 | self.modules['bounty'] = BountyModule(self.config) 48 | if self.config.farming['enabled'] and self.config.scrimmage['enabled']: 49 | self.modules['scrimmage'] = ScrimmageModule(self.config) 50 | if self.config.farming['enabled'] and self.config.mission['enabled']: 51 | self.modules['mission'] = MissionModule(self.config) 52 | if self.config.farming['enabled'] and self.config.tactical_challenge['enabled']: 53 | self.modules['tactical_challenge'] = TacticalChallengeModule(self.config) 54 | if self.config.claim_rewards['enabled']: 55 | self.modules['claim_rewards'] = ClaimRewardsModule(self.config) 56 | 57 | def run_login_cycle(self): 58 | """Method to run the login cycle. 59 | """ 60 | if self.modules['login']: 61 | return self.modules['login'].login_logic_wrapper 62 | return None 63 | 64 | def run_cafe_cycle(self): 65 | """Method to run the cafe cycle. 66 | """ 67 | if self.modules['cafe']: 68 | return self.modules['cafe'].cafe_logic_wrapper 69 | return None 70 | 71 | def run_bounty_cycle(self): 72 | """Method to run the bounty cycle. 73 | """ 74 | if self.modules['bounty']: 75 | return self.modules['bounty'].bounty_logic_wrapper 76 | return None 77 | 78 | def run_scrimmage_cycle(self): 79 | """Method to run the scrimmage cycle. 80 | """ 81 | if self.modules['scrimmage']: 82 | return self.modules['scrimmage'].scrimmage_logic_wrapper 83 | return None 84 | 85 | def run_mission_cycle(self): 86 | """Method to run the mission cycle. 87 | """ 88 | if self.modules['mission']: 89 | return self.modules['mission'].mission_logic_wrapper 90 | return None 91 | 92 | def run_tactical_challenge_cycle(self): 93 | """Method to run the tactica challenge cycle. 94 | """ 95 | if self.modules['tactical_challenge']: 96 | return self.modules['tactical_challenge'].tactical_challenge_logic_wrapper 97 | return None 98 | 99 | def run_claim_rewards_cycle(self): 100 | """Method to run the claim rewards cycle. 101 | """ 102 | if self.modules['claim_rewards']: 103 | return self.modules['claim_rewards'].claim_rewards_logic_wrapper 104 | return None 105 | 106 | # check run-time args 107 | parser = argparse.ArgumentParser() 108 | parser.add_argument('-c', '--config', 109 | metavar=('CONFIG_FILE'), 110 | help='Use the specified configuration file instead ' + 111 | 'of the default config.ini') 112 | parser.add_argument('-d', '--debug', 113 | help='Enables debugging logs.', action='store_true') 114 | parser.add_argument('-l', '--legacy', 115 | help='Enables sed usage.', action='store_true') 116 | args = parser.parse_args() 117 | # check args, and if none provided, load default config 118 | if args: 119 | if args.config: 120 | config = Config(args.config) 121 | else: 122 | config = Config('config.json') 123 | if args.debug: 124 | Logger.log_info("Enabled debugging.") 125 | Logger.enable_debugging(Logger) 126 | if args.legacy: 127 | Logger.log_info("Enabled sed usage.") 128 | Adb.enable_legacy(Adb) 129 | 130 | def terminate(): 131 | print("Terminating...") 132 | sys.exit(0) 133 | 134 | with open('traceback.log', 'w') as f: 135 | pass 136 | 137 | script = BAAuto(config) 138 | 139 | Adb.service = config.network 140 | Adb.tcp = False if (Adb.service.find(':') == -1) else True 141 | adb = Adb() 142 | 143 | def start_adb(): 144 | if adb.init(): 145 | Logger.log_msg('Successfully connected to the service with transport_id({}).'.format(Adb.transID)) 146 | output = Adb.exec_out('wm size').decode('utf-8').strip() 147 | 148 | if not re.search('1280x720|1280x720', output): 149 | Logger.log_error("Resolution is not 1280x720, please change it.") 150 | terminate() 151 | 152 | if 'com.nexon.bluearchive' not in Adb.u2device.app_list(): 153 | Logger.log_error("Blue Archive is not installed. Unable to run script.") 154 | terminate() 155 | 156 | Utils.assets = config.assets 157 | # screencap init 158 | Utils.init_screencap_mode(config.screenshot_mode) 159 | Utils.init_ocr_mode(EN=True) 160 | Utils.record['restart_attempts'] = config.restart_attempts 161 | else: 162 | if config.login["launch_emulator"]: 163 | emulator_path = config.login["emulator_path"] 164 | if os.path.isfile(emulator_path): 165 | process = subprocess.Popen(emulator_path, shell=True) 166 | Logger.log_info(f"Waiting {config.login['delay']} seconds after launching emulator...") 167 | time.sleep(config.login["delay"]) 168 | config.login["launch_emulator"] = False 169 | start_adb() 170 | return 171 | else: 172 | Logger.log_warning("Launch emulator was enabled but path is invalid.") 173 | 174 | Logger.log_error('Unable to connect to the service. Is your device connected?') 175 | terminate() 176 | 177 | start_adb() 178 | 179 | except: 180 | print(f'[ERROR] Script Initialisation Error. For more info, check the traceback.log file.') 181 | with open('traceback.log', 'w') as f: 182 | f.write(f'Script Initialisation Error') 183 | f.write('\n') 184 | traceback.print_exc(None, f, True) 185 | f.write('\n') 186 | terminate() 187 | 188 | run_cycles = [ 189 | ('Login', script.run_login_cycle), 190 | ('Cafe', script.run_cafe_cycle), 191 | ('Bounty', script.run_bounty_cycle), 192 | ('Scrimmage', script.run_scrimmage_cycle), 193 | ('Mission/Commissions', script.run_mission_cycle), 194 | ('Tactical Challenge', script.run_tactical_challenge_cycle), 195 | ('Claim Rewards', script.run_claim_rewards_cycle) 196 | ] 197 | counter = 0 198 | task_started = False 199 | task_restarted = False 200 | while counter != len(run_cycles): 201 | try: 202 | enabled = run_cycles[counter][1]() 203 | if enabled: 204 | if not task_started: 205 | Logger.log_info(f'Start Task: {run_cycles[counter][0]}') 206 | task_started = True 207 | enabled() 208 | except GameNotRunningError: 209 | if not Utils.record['game_started']: 210 | Logger.log_warning("Blue Archive is not running. Attempting to start it...") 211 | Adb.u2device.app_start("com.nexon.bluearchive", use_monkey=True) 212 | elif Utils.record['restart_attempts'] > 0: 213 | Logger.log_warning("Blue Archive crashed. Attempting to restart it...") 214 | Adb.u2device.app_start("com.nexon.bluearchive", use_monkey=True) 215 | Utils.record['restart_attempts'] -= 1 216 | Logger.log_warning(f"Restart attempts left: {Utils.record['restart_attempts']}") 217 | else: 218 | Logger.log_warning(f"Blue Archive is not running but ran out of restart attempts. Unable to restart game and run script.") 219 | terminate() 220 | Utils.reset_record() 221 | except GameStuckError: 222 | if Utils.record['restart_attempts'] > 0: 223 | Logger.log_warning("Blue Archive is stuck. Attempting to restart it...") 224 | Adb.u2device.app_stop("com.nexon.bluearchive") 225 | Adb.u2device.app_start("com.nexon.bluearchive", use_monkey=True) 226 | Utils.record['restart_attempts'] -= 1 227 | Logger.log_warning(f"Restart attempts left: {Utils.record['restart_attempts']}") 228 | Utils.reset_record() 229 | else: 230 | Logger.log_warning(f"Blue Archive is stuck but ran out of restart attempts. Unable restart game and run script.") 231 | terminate() 232 | except ReadOCRError: 233 | if not task_restarted: 234 | Logger.log_warning("Failed to read OCR. Did you change page? Restarting task...") 235 | task_restarted = True 236 | else: 237 | Logger.log_error("Failed to read OCR again. Skipping task...") 238 | counter += 1 239 | task_started = False 240 | task_restarted = False 241 | except KeyboardInterrupt: 242 | # handling ^C from user 243 | Logger.log_msg("Received keyboard interrupt from user. Closing...") 244 | terminate() 245 | except SystemExit: 246 | pass 247 | except: 248 | Logger.log_error(f'Task error: {run_cycles[counter][0]}. For more info, check the traceback.log file.') 249 | with open('traceback.log', 'a') as f: 250 | f.write(f'[{run_cycles[counter][0]}]') 251 | f.write('\n') 252 | traceback.print_exc(None, f, True) 253 | f.write('\n') 254 | counter += 1 255 | task_started = False 256 | else: 257 | if enabled: 258 | Logger.log_success(f'Task completed: {run_cycles[counter][0]}') 259 | counter += 1 260 | task_started = False 261 | 262 | Logger.log_info("All assigned tasks were executed.") 263 | -------------------------------------------------------------------------------- /traceback.log: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/traceback.log -------------------------------------------------------------------------------- /util/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedDeadDepresso/BAAuto/397c6d719969949390e08027f1c0f802717b3265/util/__init__.py -------------------------------------------------------------------------------- /util/adb.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import uiautomator2 as u2 3 | from util.logger import Logger 4 | 5 | class Adb(object): 6 | 7 | legacy = False 8 | service = '' 9 | transID = '' 10 | tcp = False 11 | u2device = None 12 | 13 | def init(self): 14 | """Kills and starts a new ADB server 15 | """ 16 | self.kill_server() 17 | return self.start_server() 18 | 19 | 20 | def enable_legacy(self): 21 | """Method to enable legacy adb usage. 22 | """ 23 | self.legacy = True 24 | return 25 | 26 | def start_server(self): 27 | """ 28 | Starts the ADB server and makes sure the android device (emulator) is attached. 29 | 30 | Returns: 31 | (boolean): True if everything is ready, False otherwise. 32 | """ 33 | cmd = ['adb', 'start-server'] 34 | subprocess.call(cmd) 35 | """ hooking onto here, previous implementation of get-state 36 | is pointless since the script kills the ADB server in advance, 37 | now seperately connect via usb or tcp, tcp variable is set by main script""" 38 | if self.tcp and self.connect_tcp(): 39 | Adb.u2device = u2.connect_adb_wifi(Adb.service) 40 | return True 41 | else: 42 | if self.connect_usb(): 43 | Adb.u2device = u2.connect_usb(Adb.service) 44 | return True 45 | return False 46 | 47 | def connect_tcp(self): 48 | cmd = ['adb', 'connect', self.service] 49 | response = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('utf-8') 50 | if (response.find('connected') == 0) or (response.find('already') == 0): 51 | self.assign_serial() 52 | if (self.transID is not None) and self.transID: 53 | return True 54 | return False 55 | 56 | def connect_usb(self): 57 | self.assign_serial() 58 | if (self.transID is not None) and self.transID: 59 | cmd = ['adb', '-t', self.transID, 'wait-for-device'] 60 | Logger.log_msg('Waiting for device [' + self.service + '] to be authorized...') 61 | subprocess.call(cmd) 62 | Logger.log_msg('Device [' + self.service + '] authorized and connected.') 63 | return True 64 | return False 65 | 66 | 67 | @staticmethod 68 | def kill_server(): 69 | """Kills the ADB server 70 | """ 71 | if Adb.u2device: 72 | Adb.u2device.disconnect() 73 | cmd = ['adb', 'kill-server'] 74 | subprocess.call(cmd) 75 | 76 | @staticmethod 77 | def exec_out(args): 78 | """Executes the command via exec-out 79 | 80 | Args: 81 | args (string): Command to execute. 82 | 83 | Returns: 84 | tuple: A tuple containing stdoutdata and stderrdata 85 | """ 86 | cmd = ['adb', '-t', Adb.transID , 'exec-out'] + args.split(' ') 87 | process = subprocess.Popen(cmd, stdout = subprocess.PIPE) 88 | return process.communicate()[0] 89 | 90 | @staticmethod 91 | def shell(args): 92 | """Executes the command via adb shell 93 | 94 | Args: 95 | args (string): Command to execute. 96 | """ 97 | cmd = ['adb', '-t', Adb.transID ,'shell'] + args.split(' ') 98 | Logger.log_debug(str(cmd)) 99 | subprocess.call(cmd) 100 | 101 | @staticmethod 102 | def cmd(args): 103 | """Executes a general command of ADB 104 | 105 | Args: 106 | args (string): Command to execute. 107 | """ 108 | cmd = ['adb', '-t', Adb.transID] + args.split(' ') 109 | Logger.log_debug(str(cmd)) 110 | process = subprocess.Popen(cmd, stdout = subprocess.PIPE) 111 | return process.communicate()[0] 112 | 113 | @classmethod 114 | def assign_serial(cls): 115 | cmd = ['adb', 'devices', '-l'] 116 | response = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('utf-8').splitlines() 117 | cls.sanitize_device_info(response) 118 | cls.transID = cls.get_serial_trans(cls.service, response) 119 | 120 | @staticmethod 121 | def sanitize_device_info(string_list): 122 | for index in range(len(string_list) - 1, -1, -1): 123 | if 'transport_id:' not in string_list[index]: 124 | string_list.pop(index) 125 | 126 | @staticmethod 127 | def get_serial_trans(device, string_list): 128 | for index in range(len(string_list)): 129 | if device in string_list[index]: 130 | return string_list[index][string_list[index].index('transport_id:') + 13:] 131 | 132 | @staticmethod 133 | def print_adb_version(): 134 | cmd = ['adb', '--version'] 135 | response = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('utf-8').splitlines() 136 | for version in response: 137 | Logger.log_error(version) 138 | -------------------------------------------------------------------------------- /util/config.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from copy import deepcopy 3 | from util.logger import Logger 4 | import util.config_consts 5 | import json 6 | 7 | class Config(object): 8 | """Config module that reads and validates the config to be passed to 9 | azurlane-auto 10 | """ 11 | 12 | def __init__(self, config_file): 13 | """Initializes the config file by changing the working directory to the 14 | root azurlane-auto folder and reading the passed-in config file. 15 | 16 | Args: 17 | config_file (string): Name of config file. 18 | """ 19 | Logger.log_msg("Initializing config module") 20 | self.config_file = config_file 21 | self.ok = False 22 | self.initialized = False 23 | self.login = {'enabled': False} 24 | self.cafe = {'enabled': False} 25 | self.farming = {'enabled': False} 26 | self.tactical_challenge = {'enabled': False} 27 | self.bounty = {'enabled': False} 28 | self.scrimmage = {'enabled': False} 29 | self.mission = {'enabled': False} 30 | self.claim_rewards = {'enabled': False} 31 | self.network = None 32 | self.assets = None 33 | self.screenshot_mode = None 34 | self.restart_attempts = 0 35 | self.read() 36 | 37 | def read(self): 38 | backup_config = deepcopy(self.__dict__) 39 | 40 | # Read the JSON file 41 | try: 42 | with open(self.config_file, 'r') as json_file: 43 | config_data = json.load(json_file) 44 | except FileNotFoundError: 45 | Logger.log_error(f"Config file '{self.config_file}' not found.") 46 | sys.exit(1) 47 | except json.JSONDecodeError: 48 | Logger.log_error(f"Invalid JSON format in '{self.config_file}'.") 49 | sys.exit(1) 50 | 51 | self.login = config_data.get('login', {'enabled': False}) 52 | self.network = config_data["login"]["network"] 53 | self.assets = config_data["login"]["server"] 54 | self.restart_attempts = config_data["login"]["restart_attempts"] 55 | self.cafe = config_data.get('cafe', {'enabled': False}) 56 | self.farming = config_data.get('farming', {'enabled':False}) 57 | self.tactical_challenge = config_data["farming"]["tactical_challenge"] 58 | self.bounty = config_data["farming"]["bounty"] 59 | self.scrimmage = config_data["farming"]["scrimmage"] 60 | self.mission = config_data["farming"]["mission"] 61 | self.claim_rewards = config_data.get('claim_rewards', {'enabled': False}) 62 | 63 | consts = util.config_consts.UtilConsts.ScreenCapMode 64 | screenshot_mode = config_data.get('login', {}).get('screenshot_mode', '').upper() 65 | vals = {'SCREENCAP_PNG':consts.SCREENCAP_PNG, 'SCREENCAP_RAW':consts.SCREENCAP_RAW, 66 | 'UIAUTOMATOR2': consts.UIAUTOMATOR2, 'ASCREENCAP':consts.ASCREENCAP} 67 | 68 | if screenshot_mode not in ['SCREENCAP_PNG', 'SCREENCAP_RAW', 'UIAUTOMATOR2', 'ASCREENCAP']: 69 | raise ValueError("Invalid screenshot mode") 70 | 71 | self.screenshot_mode = vals[screenshot_mode] 72 | 73 | self.validate() 74 | 75 | if (self.ok and not self.initialized): 76 | Logger.log_msg("Starting BAAuto!") 77 | self.initialized = True 78 | self.changed = True 79 | elif (not self.ok and not self.initialized): 80 | Logger.log_error("Invalid config. Please check your config file.") 81 | sys.exit(1) 82 | elif (not self.ok and self.initialized): 83 | Logger.log_warning("Config change detected, but with problems. Rolling back config.") 84 | self._rollback_config(backup_config) 85 | elif (self.ok and self.initialized): 86 | if backup_config != self.__dict__: 87 | Logger.log_warning("Config change detected. Hot-reloading.") 88 | self.changed = True 89 | 90 | def validate(self): 91 | """Method to validate the passed-in config file 92 | """ 93 | if not self.initialized: 94 | Logger.log_msg("Validating config") 95 | self.ok = True 96 | 97 | valid_servers = ['EN', 'CN'] 98 | if self.assets not in valid_servers: 99 | if len(valid_servers) < 2: 100 | Logger.log_error("Invalid server assets configured. Only {} is supported.".format(''.join(valid_servers))) 101 | else: 102 | Logger.log_error("Invalid server assets configured. Only {} and {} are supported.".format(', '.join(valid_servers[:-1]), valid_servers[-1])) 103 | self.ok = False 104 | 105 | if not any(module['enabled'] for module in [self.login, self.cafe, self.farming, self.claim_rewards]): 106 | Logger.log_error("All modules are disabled, consider checking your config.") 107 | self.ok = False 108 | 109 | def _rollback_config(self, config): 110 | """Method to roll back the config to the passed-in config's. 111 | Args: 112 | config (dict): previously backed up config 113 | """ 114 | for key in config: 115 | setattr(self, key, config['key']) 116 | -------------------------------------------------------------------------------- /util/config_consts.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class UtilConsts(object): 4 | 5 | def __new__(cls): 6 | return cls 7 | 8 | class ScreenCapMode(enum.Enum): 9 | SCREENCAP_PNG = enum.auto() 10 | SCREENCAP_RAW = enum.auto() 11 | UIAUTOMATOR2 = enum.auto() 12 | ASCREENCAP = enum.auto() 13 | 14 | 15 | -------------------------------------------------------------------------------- /util/emulator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pythoncom 3 | import psutil 4 | import win32com.client 5 | 6 | class Emulator: 7 | @classmethod 8 | def match_list(cls, list1, list2): 9 | for string in list1: 10 | cleaned_str = string.strip('"\'') 11 | if cleaned_str not in list2: 12 | return False 13 | return True 14 | 15 | @classmethod 16 | def extract_command_line_args(cls, emulator_path): 17 | # Resolve the emulator's actual executable path from a shortcut if provided 18 | if emulator_path.endswith('.lnk'): 19 | shell = win32com.client.Dispatch("WScript.Shell") 20 | shortcut = shell.CreateShortCut(emulator_path) 21 | emulator_path = os.path.abspath(shortcut.Targetpath) 22 | 23 | # Extract command line arguments from the shortcut 24 | arguments = shortcut.Arguments 25 | arguments = arguments.split(" ") 26 | if arguments == [""]: 27 | arguments = None 28 | else: 29 | # No .lnk file, so there are no additional arguments 30 | arguments = None 31 | 32 | return emulator_path, arguments 33 | 34 | @classmethod 35 | def terminate(cls, emulator_path): 36 | emulator_path, target_args = cls.extract_command_line_args(emulator_path) 37 | # Initialize the COM library 38 | pythoncom.CoInitialize() 39 | 40 | # Iterate over all running processes 41 | for proc in psutil.process_iter(attrs=['pid', 'name', 'exe', 'cmdline']): 42 | try: 43 | process_info = proc.info 44 | process_exe = process_info.get('exe') # Use get to handle potential None values 45 | 46 | if process_exe is not None: 47 | process_cmdline = process_info.get('cmdline', '') 48 | # Compare the executable path and command line arguments 49 | if (os.path.normcase(process_exe) == os.path.normcase(emulator_path)): 50 | if target_args is None or (process_cmdline and cls.match_list(target_args, process_cmdline)): 51 | p = psutil.Process(process_info['pid']) 52 | p.terminate() 53 | return True # Emulator process terminated successfully 54 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): 55 | pass 56 | 57 | return False # Emulator process not found or termination failed -------------------------------------------------------------------------------- /util/exceptions.py: -------------------------------------------------------------------------------- 1 | class GameStuckError(Exception): 2 | pass 3 | 4 | class GameNotRunningError(Exception): 5 | pass 6 | 7 | class ReadOCRError(Exception): 8 | pass -------------------------------------------------------------------------------- /util/logger.py: -------------------------------------------------------------------------------- 1 | # kcauto Copyright (C) 2017 Minyoung Choi 2 | 3 | from time import strftime 4 | import platform 5 | import subprocess 6 | 7 | class Logger(object): 8 | 9 | debug = False 10 | 11 | if platform.system().lower() == 'windows': 12 | subprocess.call('', shell=True) 13 | 14 | CLR_MSG = '\033[94m' 15 | CLR_SUCCESS = '\033[92m' 16 | CLR_WARNING = '\033[93m' 17 | CLR_ERROR = '\033[91m' 18 | CLR_INFO = '\u001b[35m' 19 | CLR_END = '\033[0m' 20 | 21 | def enable_debugging(self): 22 | """Method to enable debugging logs. 23 | """ 24 | self.debug = True 25 | return 26 | 27 | @staticmethod 28 | def log_format(msg): 29 | """Method to add a timestamp to a log message 30 | 31 | Args: 32 | msg (string): log msg 33 | 34 | Returns: 35 | str: log msg with timestamp appended 36 | """ 37 | return "[{}] {}".format(strftime("%Y-%m-%d %H:%M:%S"), msg) 38 | 39 | @classmethod 40 | def log_msg(cls, msg): 41 | """Method to print a log message to the console, with the 'msg' colors 42 | 43 | Args: 44 | msg (string): log msg 45 | """ 46 | print("{}[INFO] {}{}".format( 47 | cls.CLR_INFO, cls.log_format(msg), cls.CLR_END)) 48 | 49 | @classmethod 50 | def log_success(cls, msg): 51 | """Method to print a log message to the console, with the 'success' 52 | colors 53 | 54 | Args: 55 | msg (string): log msg 56 | """ 57 | print("{}[SUCCESS] {}{}".format( 58 | cls.CLR_SUCCESS, cls.log_format(msg), cls.CLR_END)) 59 | 60 | @classmethod 61 | def log_warning(cls, msg): 62 | """Method to print a log message to the console, with the 'warning' 63 | colors 64 | 65 | Args: 66 | msg (string): log msg 67 | """ 68 | print("{}[WARNING] {}{}".format( 69 | cls.CLR_WARNING, cls.log_format(msg), cls.CLR_END)) 70 | 71 | @classmethod 72 | def log_error(cls, msg): 73 | """Method to print a log message to the console, with the 'error' 74 | colors 75 | 76 | Args: 77 | msg (string): log msg 78 | """ 79 | print("{}[ERROR] {}{}".format( 80 | cls.CLR_ERROR, cls.log_format(msg), cls.CLR_END)) 81 | 82 | @classmethod 83 | def log_info(cls, msg): 84 | """Method to print a log message to the console, with the 'info' 85 | colors 86 | 87 | Args: 88 | msg (string): log msg 89 | """ 90 | print("{}[INFO] {}{}".format( 91 | cls.CLR_INFO, cls.log_format(msg), cls.CLR_END)) 92 | 93 | @classmethod 94 | def log_debug(cls, msg): 95 | """Method to print a debug message to the console, with the 'msg' 96 | colors 97 | 98 | Args: 99 | msg (string): log msg 100 | """ 101 | if not cls.debug: 102 | return 103 | print("{}[DEBUG] {}{}".format( 104 | cls.CLR_INFO, cls.log_format(msg), cls.CLR_END)) 105 | --------------------------------------------------------------------------------