├── .python-version ├── roktracker ├── __init__.py ├── honor │ ├── __init__.py │ ├── ui_settings.py │ └── scanner.py ├── seed │ ├── __init__.py │ ├── ui_settings.py │ └── scanner.py ├── utils │ ├── __init__.py │ ├── console.py │ ├── exceptions.py │ ├── output_formats.py │ ├── check_python.py │ ├── ocr.py │ ├── exception_handling.py │ ├── rok_ui_positions.py │ ├── general.py │ ├── validator.py │ ├── gui.py │ └── adb.py ├── alliance │ ├── __init__.py │ ├── governor_data.py │ ├── governor_image_group.py │ ├── additional_data.py │ ├── batch_printer.py │ ├── ui_settings.py │ ├── pandas_handler.py │ └── scanner.py └── kingdom │ ├── __init__.py │ ├── additional_data.py │ ├── governor_printer.py │ ├── pandas_handler.py │ └── governor_data.py ├── images ├── cmd-options.png ├── excel-example.png ├── example-output.png ├── bluestacks-display.png ├── platform-tools-pos.png ├── bluestacks-advanced.png └── bluestacks-performance.png ├── .vscode └── settings.json ├── deps └── inputs │ ├── ld │ ├── kingdom_1_person_scroll.txt │ ├── alliance_1_person_scroll.txt │ └── alliance_6_person_scroll.txt │ └── bluestacks │ ├── alliance_1_person_scroll.txt │ ├── honor_1_person_scroll.txt │ ├── kingdom_1_person_scroll.txt │ ├── honor_5_person_scroll.txt │ └── alliance_6_person_scroll.txt ├── dummy_root.py ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── test_bundle.yml │ └── release.yml ├── config.json ├── requirements_win32.txt ├── requirements_win64.txt ├── LICENSE ├── honor_scanner_console.py ├── seed_scanner_console.py ├── alliance_scanner_console.py ├── rok_tracker.spec ├── README.md ├── .gitignore ├── kingdom_scanner_console.py ├── honor_scanner_ui.py ├── seed_scanner_ui.py └── alliance_scanner_ui.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.12.3 -------------------------------------------------------------------------------- /roktracker/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roktracker/honor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roktracker/seed/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roktracker/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roktracker/alliance/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roktracker/kingdom/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roktracker/utils/console.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | console = Console() -------------------------------------------------------------------------------- /images/cmd-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyrexxis/RokTracker/HEAD/images/cmd-options.png -------------------------------------------------------------------------------- /images/excel-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyrexxis/RokTracker/HEAD/images/excel-example.png -------------------------------------------------------------------------------- /images/example-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyrexxis/RokTracker/HEAD/images/example-output.png -------------------------------------------------------------------------------- /images/bluestacks-display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyrexxis/RokTracker/HEAD/images/bluestacks-display.png -------------------------------------------------------------------------------- /images/platform-tools-pos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyrexxis/RokTracker/HEAD/images/platform-tools-pos.png -------------------------------------------------------------------------------- /images/bluestacks-advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyrexxis/RokTracker/HEAD/images/bluestacks-advanced.png -------------------------------------------------------------------------------- /images/bluestacks-performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyrexxis/RokTracker/HEAD/images/bluestacks-performance.png -------------------------------------------------------------------------------- /roktracker/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | class AdbError(RuntimeError): 2 | pass 3 | 4 | 5 | class ConfigError(RuntimeError): 6 | pass 7 | -------------------------------------------------------------------------------- /roktracker/alliance/governor_data.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass 5 | class GovernorData: 6 | img_path: str 7 | name: str 8 | score: str 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic", 3 | "[python]": { 4 | "editor.defaultFormatter": "ms-python.black-formatter" 5 | }, 6 | "python.formatting.provider": "none" 7 | } 8 | -------------------------------------------------------------------------------- /roktracker/alliance/governor_image_group.py: -------------------------------------------------------------------------------- 1 | from cv2.typing import MatLike 2 | from dataclasses import dataclass 3 | 4 | 5 | @dataclass 6 | class GovImageGroup: 7 | name_img: MatLike 8 | name_img_small: MatLike 9 | score_img: MatLike 10 | -------------------------------------------------------------------------------- /deps/inputs/ld/kingdom_1_person_scroll.txt: -------------------------------------------------------------------------------- 1 | 3 57 190 2 | 1 330 1 3 | 3 54 630 4 | 0 0 0 5 | 3 54 570 6 | 0 0 0 7 | 3 54 510 8 | 0 0 0 9 | 3 54 470 10 | 0 0 0 11 | 0 0 0 12 | 0 0 0 13 | 0 0 0 14 | 0 0 0 15 | 0 0 0 16 | 0 0 0 17 | 0 0 0 18 | 0 0 0 19 | 0 0 0 20 | 0 0 0 21 | 0 0 0 22 | 0 0 0 23 | 3 57 4294967295 24 | 0 0 0 -------------------------------------------------------------------------------- /deps/inputs/ld/alliance_1_person_scroll.txt: -------------------------------------------------------------------------------- 1 | 3 57 190 2 | 1 330 1 3 | 3 54 630 4 | 0 0 0 5 | 3 54 570 6 | 0 0 0 7 | 3 54 510 8 | 0 0 0 9 | 3 54 470 10 | 0 0 0 11 | 0 0 0 12 | 0 0 0 13 | 0 0 0 14 | 0 0 0 15 | 0 0 0 16 | 0 0 0 17 | 0 0 0 18 | 0 0 0 19 | 0 0 0 20 | 0 0 0 21 | 0 0 0 22 | 0 0 0 23 | 3 57 4294967295 24 | 0 0 0 -------------------------------------------------------------------------------- /dummy_root.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | 5 | def get_app_root(): 6 | if getattr(sys, "frozen", False): 7 | # If the application is run as a bundle, the PyInstaller bootloader 8 | # extends the sys module by a flag frozen=True and sets the app 9 | # path into variable _MEIPASS'. 10 | return Path(sys.executable).parent 11 | else: 12 | return Path(__file__).parent 13 | -------------------------------------------------------------------------------- /deps/inputs/bluestacks/alliance_1_person_scroll.txt: -------------------------------------------------------------------------------- 1 | 3 53 13000 2 | 3 54 20000 3 | 0 2 0 4 | 0 0 0 5 | 3 53 13000 6 | 3 54 19500 7 | 0 2 0 8 | 0 0 0 9 | 3 53 13000 10 | 3 54 19000 11 | 0 2 0 12 | 0 0 0 13 | 3 53 13000 14 | 3 54 18000 15 | 0 2 0 16 | 0 0 0 17 | 3 53 13000 18 | 3 54 15332 19 | 0 2 0 20 | 0 0 0 21 | 3 53 13000 22 | 3 54 15332 23 | 0 2 0 24 | 0 0 0 25 | 3 53 13000 26 | 3 54 15332 27 | 0 2 0 28 | 0 0 0 29 | 3 53 13000 30 | 3 54 15332 31 | 0 2 0 32 | 0 0 0 33 | 0 2 0 34 | 0 0 0 -------------------------------------------------------------------------------- /deps/inputs/bluestacks/honor_1_person_scroll.txt: -------------------------------------------------------------------------------- 1 | 3 53 13000 2 | 3 54 20000 3 | 0 2 0 4 | 0 0 0 5 | 3 53 13000 6 | 3 54 19500 7 | 0 2 0 8 | 0 0 0 9 | 3 53 13000 10 | 3 54 19000 11 | 0 2 0 12 | 0 0 0 13 | 3 53 13000 14 | 3 54 18000 15 | 0 2 0 16 | 0 0 0 17 | 3 53 13000 18 | 3 54 15368 19 | 0 2 0 20 | 0 0 0 21 | 3 53 13000 22 | 3 54 15368 23 | 0 2 0 24 | 0 0 0 25 | 3 53 13000 26 | 3 54 15368 27 | 0 2 0 28 | 0 0 0 29 | 3 53 13000 30 | 3 54 15368 31 | 0 2 0 32 | 0 0 0 33 | 0 2 0 34 | 0 0 0 35 | -------------------------------------------------------------------------------- /deps/inputs/bluestacks/kingdom_1_person_scroll.txt: -------------------------------------------------------------------------------- 1 | 3 53 13000 2 | 3 54 20000 3 | 0 2 0 4 | 0 0 0 5 | 3 53 13000 6 | 3 54 19500 7 | 0 2 0 8 | 0 0 0 9 | 3 53 13000 10 | 3 54 19000 11 | 0 2 0 12 | 0 0 0 13 | 3 53 13000 14 | 3 54 18000 15 | 0 2 0 16 | 0 0 0 17 | 3 53 13000 18 | 3 54 15332 19 | 0 2 0 20 | 0 0 0 21 | 3 53 13000 22 | 3 54 15332 23 | 0 2 0 24 | 0 0 0 25 | 3 53 13000 26 | 3 54 15332 27 | 0 2 0 28 | 0 0 0 29 | 3 53 13000 30 | 3 54 15332 31 | 0 2 0 32 | 0 0 0 33 | 0 2 0 34 | 0 0 0 -------------------------------------------------------------------------------- /deps/inputs/ld/alliance_6_person_scroll.txt: -------------------------------------------------------------------------------- 1 | 3 57 102 2 | 1 330 1 3 | 3 54 790 4 | 0 0 0 5 | 3 54 730 6 | 0 0 0 7 | 3 54 670 8 | 0 0 0 9 | 3 54 610 10 | 0 0 0 11 | 3 54 550 12 | 0 0 0 13 | 3 54 490 14 | 0 0 0 15 | 3 54 430 16 | 0 0 0 17 | 3 54 370 18 | 0 0 0 19 | 3 54 310 20 | 0 0 0 21 | 3 54 250 22 | 0 0 0 23 | 3 54 190 24 | 0 0 0 25 | 3 54 125 26 | 0 0 0 27 | 0 0 0 28 | 0 0 0 29 | 0 0 0 30 | 0 0 0 31 | 0 0 0 32 | 0 0 0 33 | 0 0 0 34 | 0 0 0 35 | 0 0 0 36 | 0 0 0 37 | 0 0 0 38 | 3 57 4294967295 39 | 0 0 0 -------------------------------------------------------------------------------- /deps/inputs/bluestacks/honor_5_person_scroll.txt: -------------------------------------------------------------------------------- 1 | 3 53 13000 2 | 3 54 28000 3 | 0 2 0 4 | 0 0 0 5 | 3 53 13000 6 | 3 54 27500 7 | 0 2 0 8 | 0 0 0 9 | 3 53 13000 10 | 3 54 27000 11 | 0 2 0 12 | 0 0 0 13 | 3 53 13000 14 | 3 54 18000 15 | 0 2 0 16 | 0 0 0 17 | 3 53 13000 18 | 3 54 12000 19 | 0 2 0 20 | 0 0 0 21 | 3 53 13000 22 | 3 54 8820 23 | 0 2 0 24 | 0 0 0 25 | 3 53 13000 26 | 3 54 8820 27 | 0 2 0 28 | 0 0 0 29 | 3 53 13000 30 | 3 54 8820 31 | 0 2 0 32 | 0 0 0 33 | 3 53 13000 34 | 3 54 8820 35 | 0 2 0 36 | 0 0 0 37 | 0 2 0 38 | 0 0 0 -------------------------------------------------------------------------------- /deps/inputs/bluestacks/alliance_6_person_scroll.txt: -------------------------------------------------------------------------------- 1 | 3 53 13000 2 | 3 54 28000 3 | 0 2 0 4 | 0 0 0 5 | 3 53 13000 6 | 3 54 27500 7 | 0 2 0 8 | 0 0 0 9 | 3 53 13000 10 | 3 54 27000 11 | 0 2 0 12 | 0 0 0 13 | 3 53 13000 14 | 3 54 18000 15 | 0 2 0 16 | 0 0 0 17 | 3 53 13000 18 | 3 54 12000 19 | 0 2 0 20 | 0 0 0 21 | 3 53 13000 22 | 3 54 5000 23 | 0 2 0 24 | 0 0 0 25 | 3 53 13000 26 | 3 54 5000 27 | 0 2 0 28 | 0 0 0 29 | 3 53 13000 30 | 3 54 5000 31 | 0 2 0 32 | 0 0 0 33 | 3 53 13000 34 | 3 54 5000 35 | 0 2 0 36 | 0 0 0 37 | 0 2 0 38 | 0 0 0 -------------------------------------------------------------------------------- /roktracker/alliance/additional_data.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from dataclasses import dataclass 4 | 5 | from roktracker.utils.general import format_timedelta_to_HHMMSS 6 | 7 | 8 | @dataclass 9 | class AdditionalData: 10 | current_page: int 11 | target_governor: int 12 | govs_per_page: int 13 | remaining_sec: float 14 | current_time: str = datetime.datetime.now().strftime("%H:%M:%S") 15 | 16 | def eta(self) -> str: 17 | return format_timedelta_to_HHMMSS( 18 | datetime.timedelta(seconds=self.remaining_sec) 19 | ) 20 | -------------------------------------------------------------------------------- /roktracker/kingdom/additional_data.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from dataclasses import dataclass 4 | 5 | from roktracker.utils.general import format_timedelta_to_HHMMSS 6 | 7 | 8 | @dataclass 9 | class AdditionalData: 10 | current_governor: int 11 | target_governor: int 12 | skipped_governors: int 13 | power_ok: str 14 | kills_ok: str 15 | reconstruction_success: str 16 | remaining_sec: float 17 | current_time: str = datetime.datetime.now().strftime("%H:%M:%S") 18 | 19 | def eta(self) -> str: 20 | return format_timedelta_to_HHMMSS( 21 | datetime.timedelta(seconds=self.remaining_sec) 22 | ) 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /roktracker/honor/ui_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Tuple 3 | from dummy_root import get_app_root 4 | 5 | 6 | @dataclass 7 | class HonorMisc: 8 | threshold: int = 150 9 | invert: bool = False 10 | script: str = "honor_5_person_scroll.txt" 11 | 12 | 13 | class HonorUI: 14 | name: List[Tuple[int, int, int, int]] = [ 15 | (774, 324, 257, 40), 16 | (774, 424, 257, 40), 17 | (774, 524, 257, 40), 18 | (774, 624, 257, 40), 19 | (774, 724, 257, 40), 20 | ] 21 | score: List[Tuple[int, int, int, int]] = [ 22 | (1183, 324, 178, 40), 23 | (1183, 424, 178, 40), 24 | (1183, 524, 178, 40), 25 | (1183, 624, 178, 40), 26 | (1183, 724, 178, 40), 27 | ] 28 | misc = HonorMisc() 29 | -------------------------------------------------------------------------------- /roktracker/utils/output_formats.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict, List 3 | 4 | 5 | @dataclass 6 | class OutputFormats: 7 | xlsx: bool = False 8 | csv: bool = False 9 | jsonl: bool = False 10 | 11 | def from_list(self, list: List[str]): 12 | for item in list: 13 | if item == "xlsx": 14 | self.xlsx = True 15 | elif item == "csv": 16 | self.csv = True 17 | elif item == "jsonl": 18 | self.jsonl = True 19 | 20 | def from_dict(self, dict: Dict[str, bool]): 21 | for key, value in dict.items(): 22 | if key == "xlsx": 23 | self.xlsx = value 24 | elif key == "csv": 25 | self.csv = value 26 | elif key == "jsonl": 27 | self.jsonl = value 28 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "scan": { 3 | "kingdom_name": "", 4 | "people_to_scan": "", 5 | "resume": false, 6 | "advanced_scroll": true, 7 | "track_inactives": false, 8 | "validate_power": false, 9 | "power_threshold": 100000, 10 | "validate_kills": true, 11 | "reconstruct_kills": true, 12 | "timings": { 13 | "gov_open": 2, 14 | "copy_wait": 0.2, 15 | "kills_open": 1, 16 | "info_open": 1, 17 | "info_close": 0.5, 18 | "gov_close": 1, 19 | "max_random": 0.5 20 | }, 21 | "formats": { 22 | "xlsx": true, 23 | "csv": false, 24 | "jsonl": false 25 | } 26 | }, 27 | "general": { 28 | "emulator": "bluestacks", 29 | "bluestacks": { 30 | "name": "RoK Tracker", 31 | "config": "C:\\ProgramData\\BlueStacks_nxt\\bluestacks.conf" 32 | }, 33 | "adb_port": 5555 34 | } 35 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Your Setup (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Bluestacks Version: [e.g. 5.11.1.1002] 29 | - Android of Bluestacks: [e.g. Pie 64-bit] 30 | - Tracker Version: [e.g. commit hash or version] 31 | 32 | **Additional context** 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /roktracker/utils/check_python.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | from roktracker.utils.console import console 6 | from typing import Tuple 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | def check_py_version(required_version: Tuple[int, int]) -> bool: 12 | current_version = sys.version_info 13 | 14 | if current_version < required_version: 15 | console.log( 16 | f"Update required: Current Python version is {current_version.major}.{current_version.minor} " 17 | f"but version {required_version[0]}.{required_version[1]} or higher is needed." 18 | ) 19 | logger.warning( 20 | f"Update required: Current Python version is {current_version.major}.{current_version.minor} " 21 | f"but version {required_version[0]}.{required_version[1]} or higher is needed." 22 | ) 23 | return False 24 | else: 25 | return True 26 | -------------------------------------------------------------------------------- /requirements_win32.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17.4 2 | androidviewclient==23.0.1 3 | certifi==2023.11.17 4 | charset-normalizer==3.3.2 5 | contourpy==1.2.0 6 | culebratester-client==2.0.64 7 | customtkinter==5.2.1 8 | cycler==0.12.1 9 | darkdetect==0.8.0 10 | et-xmlfile==1.1.0 11 | fonttools==4.46.0 12 | idna==3.6 13 | kiwisolver==1.4.5 14 | markdown-it-py==3.0.0 15 | matplotlib==3.8.2 16 | mdurl==0.1.2 17 | numpy==1.26.2 18 | opencv-python-headless==4.8.1.78 19 | openpyxl==3.1.2 20 | packaging==23.2 21 | pandas==2.2.1 22 | pathvalidate==3.2.0 23 | pefile==2023.2.7 24 | Pillow==10.1.0 25 | prompt-toolkit==3.0.36 26 | Pygments==2.17.2 27 | pyinstaller==6.3.0 28 | pyinstaller-hooks-contrib==2023.11 29 | pyparsing==3.1.1 30 | python-dateutil==2.8.2 31 | pytz==2024.1 32 | pywin32-ctypes==0.2.2 33 | questionary==2.0.1 34 | requests==2.31.0 35 | rich==13.7.0 36 | six==1.16.0 37 | tesserocr @ https://github.com/simonflueckiger/tesserocr-windows_build/releases/download/tesserocr-v2.6.2-tesseract-5.3.4/tesserocr-2.6.2-cp312-cp312-win32.whl 38 | tzdata==2024.1 39 | urllib3==2.1.0 40 | wcwidth==0.2.12 41 | XlsxWriter==3.2.0 42 | -------------------------------------------------------------------------------- /requirements_win64.txt: -------------------------------------------------------------------------------- 1 | altgraph==0.17.4 2 | androidviewclient==23.0.1 3 | certifi==2023.11.17 4 | charset-normalizer==3.3.2 5 | contourpy==1.2.0 6 | culebratester-client==2.0.64 7 | customtkinter==5.2.1 8 | cycler==0.12.1 9 | darkdetect==0.8.0 10 | et-xmlfile==1.1.0 11 | fonttools==4.46.0 12 | idna==3.6 13 | kiwisolver==1.4.5 14 | markdown-it-py==3.0.0 15 | matplotlib==3.8.2 16 | mdurl==0.1.2 17 | numpy==1.26.2 18 | opencv-python-headless==4.8.1.78 19 | openpyxl==3.1.2 20 | packaging==23.2 21 | pandas==2.2.1 22 | pathvalidate==3.2.0 23 | pefile==2023.2.7 24 | Pillow==10.1.0 25 | prompt-toolkit==3.0.36 26 | Pygments==2.17.2 27 | pyinstaller==6.3.0 28 | pyinstaller-hooks-contrib==2023.11 29 | pyparsing==3.1.1 30 | python-dateutil==2.8.2 31 | pytz==2024.1 32 | pywin32-ctypes==0.2.2 33 | questionary==2.0.1 34 | requests==2.31.0 35 | rich==13.7.0 36 | six==1.16.0 37 | tesserocr @ https://github.com/simonflueckiger/tesserocr-windows_build/releases/download/tesserocr-v2.6.2-tesseract-5.3.4/tesserocr-2.6.2-cp312-cp312-win_amd64.whl 38 | tzdata==2024.1 39 | urllib3==2.1.0 40 | wcwidth==0.2.12 41 | XlsxWriter==3.2.0 42 | -------------------------------------------------------------------------------- /roktracker/alliance/batch_printer.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from roktracker.alliance.additional_data import AdditionalData 3 | 4 | from roktracker.alliance.governor_data import GovernorData 5 | from roktracker.utils.console import console 6 | 7 | from rich.markup import escape 8 | from rich.table import Table 9 | 10 | 11 | def print_batch(govs: List[GovernorData], extra: AdditionalData) -> None: 12 | # nice output for console 13 | table = Table( 14 | title="[" 15 | + extra.current_time 16 | + "]\n" 17 | + "Latest Scan Result\nGovernor " 18 | + f"{extra.current_page * extra.govs_per_page} - {extra.current_page * extra.govs_per_page + len(govs)}" 19 | + " of " 20 | + str(extra.target_governor), 21 | show_header=True, 22 | show_footer=True, 23 | ) 24 | table.add_column("Governor Name", "Approx time remaining", style="magenta") 25 | table.add_column("Score", extra.eta(), style="cyan") 26 | 27 | for governor in govs: 28 | table.add_row(escape(governor.name), str(governor.score)) 29 | 30 | console.print(table) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 nikolakis1919 4 | Copyright (c) 2022-2024 Cyrexxis 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /roktracker/seed/ui_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Tuple 3 | from dummy_root import get_app_root 4 | 5 | 6 | @dataclass 7 | class KingdomMisc: 8 | threshold: int = 90 9 | invert: bool = True 10 | script: str = "alliance_6_person_scroll.txt" 11 | 12 | 13 | class KingdomUI: 14 | name_normal: List[Tuple[int, int, int, int]] = [ 15 | (334, 260, 293, 33), 16 | (334, 359, 293, 33), 17 | (334, 460, 293, 33), 18 | (334, 562, 293, 33), 19 | (334, 662, 293, 33), 20 | (334, 763, 293, 33), 21 | ] 22 | score_normal: List[Tuple[int, int, int, int]] = [ 23 | (1087, 269, 250, 33), 24 | (1087, 368, 250, 33), 25 | (1087, 469, 250, 33), 26 | (1087, 571, 250, 33), 27 | (1087, 671, 250, 33), 28 | (1087, 772, 250, 33), 29 | ] 30 | name_last: List[Tuple[int, int, int, int]] = [ 31 | (334, 283, 293, 33), 32 | (334, 383, 293, 33), 33 | (334, 483, 293, 33), 34 | (334, 585, 293, 33), 35 | (334, 685, 293, 33), 36 | (334, 785, 293, 33), 37 | ] 38 | score_last: List[Tuple[int, int, int, int]] = [ 39 | (1087, 292, 250, 33), 40 | (1087, 392, 250, 33), 41 | (1087, 492, 250, 33), 42 | (1087, 594, 250, 33), 43 | (1087, 694, 250, 33), 44 | (1087, 794, 250, 33), 45 | ] 46 | misc = KingdomMisc() 47 | -------------------------------------------------------------------------------- /roktracker/alliance/ui_settings.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Tuple 3 | from dummy_root import get_app_root 4 | 5 | 6 | @dataclass 7 | class AllianceMisc: 8 | threshold: int = 90 9 | invert: bool = True 10 | script: str = "alliance_6_person_scroll.txt" 11 | 12 | 13 | class AllianceUI: 14 | name_normal: List[Tuple[int, int, int, int]] = [ 15 | (334, 260, 293, 33), 16 | (334, 359, 293, 33), 17 | (334, 460, 293, 33), 18 | (334, 562, 293, 33), 19 | (334, 662, 293, 33), 20 | (334, 763, 293, 33), 21 | ] 22 | score_normal: List[Tuple[int, int, int, int]] = [ 23 | (1117, 265, 250, 33), 24 | (1117, 364, 250, 33), 25 | (1117, 465, 250, 33), 26 | (1117, 567, 250, 33), 27 | (1117, 667, 250, 33), 28 | (1117, 768, 250, 33), 29 | ] 30 | name_last: List[Tuple[int, int, int, int]] = [ 31 | (334, 283, 293, 33), 32 | (334, 383, 293, 33), 33 | (334, 483, 293, 33), 34 | (334, 585, 293, 33), 35 | (334, 685, 293, 33), 36 | (334, 785, 293, 33), 37 | ] 38 | score_last: List[Tuple[int, int, int, int]] = [ 39 | (1117, 288, 250, 33), 40 | (1117, 388, 250, 33), 41 | (1117, 488, 250, 33), 42 | (1117, 590, 250, 33), 43 | (1117, 690, 250, 33), 44 | (1117, 790, 250, 33), 45 | ] 46 | misc = AllianceMisc() 47 | -------------------------------------------------------------------------------- /.github/workflows/test_bundle.yml: -------------------------------------------------------------------------------- 1 | name: Package Application with Pyinstaller 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-win64: 8 | runs-on: windows-latest 9 | steps: 10 | - name: (Install) python 11 | uses: actions/setup-python@v4 12 | with: 13 | python-version: "3.11" 14 | architecture: "x64" 15 | 16 | - name: (Install) python dev tools 17 | shell: bash 18 | run: python -m pip install pip wheel setuptools 19 | 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: (Install) dependencies 24 | run: python -m pip install -r "requirements_win64.txt" 25 | shell: bash 26 | 27 | - name: (Install) pyinstaller 28 | shell: bash 29 | run: | 30 | set PYINSTALLER_COMPILE_BOOTLOADER=TRUE 31 | pip install pyinstaller 32 | 33 | - name: (Create) Executable 34 | shell: bash 35 | run: | 36 | pyinstaller \ 37 | --clean \ 38 | --noconfirm \ 39 | rok_tracker.spec 40 | 41 | echo "✔️ Executables created successfully" >> $GITHUB_STEP_SUMMARY 42 | echo " - Python version used: '3.11'" >> $GITHUB_STEP_SUMMARY 43 | echo " - Python architecture used: 'x64'" >> $GITHUB_STEP_SUMMARY 44 | 45 | - name: Prepare executable for upload 46 | run: | 47 | cp "config.json" "dist/RoK Tracker/config.json" 48 | mkdir "dist/RoK Tracker/deps" 49 | cp -r "deps/inputs/" "dist/RoK Tracker/deps/" 50 | tar.exe -a -c -f "RoK Tracker.zip" -C "dist/" "RoK Tracker" 51 | 52 | - name: Upload Artifact 53 | uses: actions/upload-artifact@v3 54 | with: 55 | name: RoK Tracker 56 | path: "RoK Tracker.zip" -------------------------------------------------------------------------------- /roktracker/utils/ocr.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import re 3 | import tesserocr 4 | 5 | from PIL import Image 6 | from cv2.typing import MatLike 7 | from typing import Tuple 8 | 9 | 10 | def cropToRegion(image: MatLike, roi: Tuple[int, int, int, int]) -> MatLike: 11 | return image[int(roi[1]) : int(roi[1] + roi[3]), int(roi[0]) : int(roi[0] + roi[2])] 12 | 13 | 14 | def cropToTextWithBorder(img: MatLike, border_size) -> MatLike: 15 | coords = cv2.findNonZero(cv2.bitwise_not(img)) 16 | x, y, w, h = cv2.boundingRect(coords) 17 | 18 | roi = img[y : y + h, x : x + w] 19 | bordered = cv2.copyMakeBorder( 20 | roi, 21 | top=border_size, 22 | bottom=border_size, 23 | left=border_size, 24 | right=border_size, 25 | borderType=cv2.BORDER_CONSTANT, 26 | value=[255], 27 | ) 28 | 29 | return bordered 30 | 31 | 32 | def preprocessImage( 33 | image: MatLike, 34 | scale_factor: int, 35 | threshold: int, 36 | border_size: int, 37 | invert: bool = False, 38 | ) -> MatLike: 39 | im_big = cv2.resize(image, (0, 0), fx=scale_factor, fy=scale_factor) 40 | im_gray = cv2.cvtColor(im_big, cv2.COLOR_BGR2GRAY) 41 | if invert: 42 | im_gray = cv2.bitwise_not(im_gray) 43 | (thresh, im_bw) = cv2.threshold(im_gray, threshold, 255, cv2.THRESH_BINARY) 44 | im_bw = cropToTextWithBorder(im_bw, border_size) 45 | return im_bw 46 | 47 | 48 | def ocr_number(api, image: MatLike): 49 | api.SetImage(Image.fromarray(image)) 50 | score = api.GetUTF8Text() 51 | score = re.sub("[^0-9]", "", score) 52 | return score 53 | 54 | 55 | def ocr_text(api, image: MatLike): 56 | api.SetImage(Image.fromarray(image)) 57 | name = api.GetUTF8Text() 58 | return name.rstrip("\n") 59 | 60 | 61 | def preprocess_and_ocr_number( 62 | api, image: MatLike, region: Tuple[int, int, int, int], invert: bool = False 63 | ): 64 | cropped_image = cropToRegion(image, region) 65 | cropped_bw_image = preprocessImage(cropped_image, 3, 150, 12, invert) 66 | 67 | return ocr_number(api, cropped_bw_image) 68 | 69 | 70 | def get_supported_langs(path: str) -> str: 71 | return str(tesserocr.get_languages(path)) # type: ignore 72 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Package Application with Pyinstaller and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build-win64: 10 | runs-on: windows-latest 11 | steps: 12 | - name: (Install) python 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: "3.12" 16 | architecture: "x64" 17 | 18 | - name: (Install) python dev tools 19 | shell: bash 20 | run: python -m pip install pip wheel setuptools 21 | 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: (Install) dependencies 26 | run: python -m pip install -r "requirements_win64.txt" 27 | shell: bash 28 | 29 | - name: (Install) pyinstaller 30 | shell: bash 31 | run: | 32 | set PYINSTALLER_COMPILE_BOOTLOADER=TRUE 33 | pip install pyinstaller 34 | 35 | - name: (Create) Executable 36 | shell: bash 37 | run: | 38 | pyinstaller \ 39 | --clean \ 40 | --noconfirm \ 41 | rok_tracker.spec 42 | 43 | echo "✔️ Executables created successfully" >> $GITHUB_STEP_SUMMARY 44 | echo " - Python version used: '3.12'" >> $GITHUB_STEP_SUMMARY 45 | echo " - Python architecture used: 'x64'" >> $GITHUB_STEP_SUMMARY 46 | 47 | - name: Prepare executable for upload 48 | run: | 49 | cp "config.json" "dist/RoK Tracker/config.json" 50 | mkdir "dist/RoK Tracker/deps" 51 | cp -r "deps/inputs/" "dist/RoK Tracker/deps/" 52 | tar.exe -a -c -f "RoK Tracker.zip" -C "dist/" "RoK Tracker" 53 | 54 | - name: Upload Artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: RoK Tracker 58 | path: "RoK Tracker.zip" 59 | 60 | release: 61 | runs-on: ubuntu-latest 62 | needs: 63 | - build-win64 64 | 65 | steps: 66 | - name: Get compiled executables 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: RoK Tracker 70 | 71 | - name: Release 72 | uses: softprops/action-gh-release@v0.1.15 73 | with: 74 | files: | 75 | RoK Tracker.zip 76 | -------------------------------------------------------------------------------- /roktracker/utils/exception_handling.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | from com.dtmilano.android.adb.adbclient import Timer 4 | from roktracker.utils.console import console 5 | from roktracker.utils.gui import ConfirmDialog, InfoDialog 6 | from threading import ExceptHookArgs 7 | 8 | 9 | class ConsoleExceptionHander: 10 | def __init__(self, logger: logging.Logger): 11 | self.logger = logger 12 | 13 | def handle_exception(self, exc_type, exc_value, exc_traceback): 14 | if issubclass(exc_type, KeyboardInterrupt): 15 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 16 | return 17 | 18 | # needed because of how adb library is implemented... 19 | if issubclass(exc_type, Timer.TimeoutException): 20 | # no sys excepthook to prevent error shown in the console 21 | return 22 | 23 | self.logger.critical( 24 | "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback) 25 | ) 26 | 27 | console.print( 28 | "A critical error occured and the program stopped. For more info view the log file." 29 | ) 30 | 31 | def handle_thread_exception(self, exc: ExceptHookArgs): 32 | self.handle_exception(exc.exc_type, exc.exc_value, exc.exc_traceback) 33 | 34 | 35 | class GuiExceptionHandler: 36 | def __init__(self, logger: logging.Logger): 37 | self.logger = logger 38 | 39 | def handle_exception(self, exc_type, exc_value, exc_traceback): 40 | if issubclass(exc_type, KeyboardInterrupt): 41 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 42 | return 43 | 44 | # needed because of how adb library is implemented... 45 | if issubclass(exc_type, Timer.TimeoutException): 46 | return 47 | 48 | self.logger.critical( 49 | "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback) 50 | ) 51 | InfoDialog( 52 | "Error", 53 | "An error occured, see the log file for more info.\nYou probably have to restart this application.", 54 | "300x140", 55 | ) 56 | 57 | def handle_thread_exception(self, exc: ExceptHookArgs): 58 | self.handle_exception(exc.exc_type, exc.exc_value, exc.exc_traceback) 59 | -------------------------------------------------------------------------------- /roktracker/utils/rok_ui_positions.py: -------------------------------------------------------------------------------- 1 | ocr_check_profile_version = (877, 356, 85, 26) # new profile UI has "Acclaim" text here 2 | 3 | # format: (x, y, width, height) 4 | ocr_regions_old = { 5 | # first screen 6 | "more_info": (178, 788, 137, 29), 7 | "gov_id": (721, 177, 260, 38), 8 | "power": (876, 321, 218, 38), 9 | "killpoints": (1185, 313, 216, 38), 10 | "alliance_name": (586, 321, 279, 38), 11 | # second screen 12 | "t1_kills": (917, 414, 200, 38), 13 | "t2_kills": (917, 458, 200, 38), 14 | "t3_kills": (917, 502, 200, 38), 15 | "t4_kills": (917, 546, 200, 38), 16 | "t5_kills": (917, 591, 200, 38), 17 | "t1_killpoints": (1298, 414, 171, 38), 18 | "t2_killpoints": (1298, 458, 171, 38), 19 | "t3_killpoints": (1298, 502, 171, 38), 20 | "t4_killpoints": (1298, 546, 171, 38), 21 | "t5_killpoints": (1298, 591, 171, 38), 22 | "ranged_points": (1275, 693, 200, 38), 23 | # third screen 24 | "deads": (1130, 453, 183, 40), 25 | "rss_gathered": (1130, 628, 183, 40), 26 | "rss_assisted": (1130, 688, 183, 40), 27 | "alliance_helps": (1130, 746, 183, 40), 28 | } 29 | 30 | # format: (x, y, width, height) 31 | ocr_regions = { 32 | # first screen 33 | "more_info": (178, 788, 137, 29), 34 | "gov_id": (714, 177, 260, 38), 35 | "power": (1142, 306, 218, 38), 36 | "killpoints": (876, 306, 216, 38), 37 | "alliance_name": (586, 306, 279, 38), 38 | # second screen 39 | "t1_kills": (607, 397, 200, 38), 40 | "t2_kills": (607, 441, 200, 38), 41 | "t3_kills": (607, 485, 200, 38), 42 | "t4_kills": (607, 529, 200, 38), 43 | "t5_kills": (607, 574, 200, 38), 44 | "t1_killpoints": (988, 397, 171, 38), 45 | "t2_killpoints": (988, 441, 171, 38), 46 | "t3_killpoints": (988, 485, 171, 38), 47 | "t4_killpoints": (988, 529, 171, 38), 48 | "t5_killpoints": (988, 574, 171, 38), 49 | "ranged_points": (965, 676, 200, 38), 50 | # third screen 51 | "deads": (1130, 453, 183, 40), 52 | "rss_gathered": (1130, 628, 183, 40), 53 | "rss_assisted": (1130, 688, 183, 40), 54 | "alliance_helps": (1130, 746, 183, 40), 55 | } 56 | 57 | # format: (x, y) 58 | tap_positions_old = { 59 | # first screen 60 | "name_copy": (617, 232), 61 | "open_kills": (1174, 305), 62 | "more_info": (242, 746), 63 | "close_gov": (1454, 88), 64 | # third screen 65 | "close_info": (1396, 58), 66 | } 67 | 68 | # format: (x, y) 69 | tap_positions = { 70 | # first screen 71 | "name_copy": (617, 237), 72 | "open_kills": (864, 288), 73 | "more_info": (242, 746), 74 | "close_gov": (1454, 88), 75 | # third screen 76 | "close_info": (1396, 58), 77 | } -------------------------------------------------------------------------------- /roktracker/kingdom/governor_printer.py: -------------------------------------------------------------------------------- 1 | from rich.markup import escape 2 | from rich.table import Table 3 | from roktracker.kingdom.additional_data import AdditionalData 4 | from roktracker.kingdom.governor_data import GovernorData 5 | from roktracker.utils.console import console 6 | 7 | 8 | def print_gov_state(gov_data: GovernorData, extra: AdditionalData) -> None: 9 | table = Table( 10 | title="[" 11 | + extra.current_time 12 | + "]\n" 13 | + "Latest Scan Result\nGovernor " 14 | + str(extra.current_governor) 15 | + " of " 16 | + str(extra.target_governor), 17 | show_header=True, 18 | show_footer=True, 19 | ) 20 | 21 | first_title = "Entry" 22 | second_title = "Value" 23 | 24 | first_footer = "Approx time remaining\nSkipped\n" 25 | second_footer = str(extra.eta()) + "\n" + str(extra.skipped_governors) + "\n" 26 | 27 | if extra.power_ok != "Not Checked": 28 | first_footer += "Power ok\n" 29 | second_footer += str(extra.power_ok) + "\n" 30 | 31 | if extra.kills_ok != "Not Checked": 32 | first_footer += "Kills check out\n" 33 | second_footer += str(extra.kills_ok) + "\n" 34 | 35 | if (not extra.kills_ok) and extra.reconstruction_success != "Not Checked": 36 | first_footer += "Reconstruct success\n" 37 | second_footer += str(extra.reconstruction_success) + "\n" 38 | 39 | table.add_column( 40 | first_title, 41 | first_footer.rstrip(), 42 | style="magenta", 43 | ) 44 | table.add_column( 45 | second_title, 46 | second_footer.rstrip(), 47 | style="cyan", 48 | ) 49 | 50 | table.add_row("Governor ID", gov_data.id) 51 | table.add_row("Governor Name", gov_data.name) 52 | table.add_row("Governor Power", gov_data.power) 53 | table.add_row("Governor Kill Points", gov_data.killpoints) 54 | table.add_row("Governor Deads", gov_data.dead) 55 | table.add_row("Governor T1 Kills", gov_data.t1_kills) 56 | table.add_row("Governor T2 Kills", gov_data.t2_kills) 57 | table.add_row("Governor T3 Kills", gov_data.t3_kills) 58 | table.add_row("Governor T4 Kills", gov_data.t4_kills) 59 | table.add_row("Governor T5 Kills", gov_data.t5_kills) 60 | table.add_row("Governor T4+5 Kills", gov_data.t45_kills()) 61 | table.add_row("Governor Total Kills", gov_data.total_kills()) 62 | table.add_row("Governor Ranged Points", gov_data.ranged_points) 63 | table.add_row("Governor RSS Assistance", gov_data.rss_assistance) 64 | table.add_row("Governor RSS Gathered", gov_data.rss_gathered) 65 | table.add_row("Governor Helps", gov_data.helps) 66 | table.add_row("Governor Alliance", escape(gov_data.alliance)) 67 | 68 | console.print(table) 69 | -------------------------------------------------------------------------------- /roktracker/kingdom/pandas_handler.py: -------------------------------------------------------------------------------- 1 | from os import PathLike 2 | from typing import Any 3 | import pandas as pd 4 | import pathlib 5 | 6 | from roktracker.kingdom.governor_data import GovernorData 7 | from roktracker.utils.output_formats import OutputFormats 8 | from datetime import date 9 | 10 | 11 | class PandasHandler: 12 | def __init__( 13 | self, 14 | path: str | PathLike[Any], 15 | filename: str, 16 | formats: OutputFormats, 17 | title: str = str(date.today()), 18 | ): 19 | self.title = title 20 | self.path = pathlib.Path(path) 21 | self.name = filename 22 | self.formats = formats 23 | self.data_list = [] 24 | 25 | def write_governor(self, gov_data: GovernorData) -> None: 26 | self.data_list.append( 27 | { 28 | "ID": GovernorData.intify_value(gov_data.id), 29 | "Name": gov_data.name, 30 | "Power": GovernorData.intify_value(gov_data.power), 31 | "Killpoints": GovernorData.intify_value(gov_data.killpoints), 32 | "Deads": GovernorData.intify_value(gov_data.dead), 33 | "T1 Kills": GovernorData.intify_value(gov_data.t1_kills), 34 | "T2 Kills": GovernorData.intify_value(gov_data.t2_kills), 35 | "T3 Kills": GovernorData.intify_value(gov_data.t3_kills), 36 | "T4 Kills": GovernorData.intify_value(gov_data.t4_kills), 37 | "T5 Kills": GovernorData.intify_value(gov_data.t5_kills), 38 | "Total Kills": GovernorData.intify_value(gov_data.total_kills()), 39 | "T45 Kills": GovernorData.intify_value(gov_data.t45_kills()), 40 | "Ranged": GovernorData.intify_value(gov_data.ranged_points), 41 | "Rss Gathered": GovernorData.intify_value(gov_data.rss_gathered), 42 | "Rss Assistance": GovernorData.intify_value(gov_data.rss_assistance), 43 | "Helps": GovernorData.intify_value(gov_data.helps), 44 | "Alliance": gov_data.alliance.rstrip(), 45 | } 46 | ) 47 | 48 | def is_duplicate(self, gov_id: int) -> bool: 49 | if len(self.data_list) == 0: 50 | return False 51 | elif self.data_list[-1]["ID"] == gov_id: 52 | return True 53 | else: 54 | return False 55 | 56 | def save(self): 57 | frame = pd.DataFrame(self.data_list) 58 | # Drop cols that contain skipped values 59 | frame = frame.loc[:, ~(frame == -2).any()] 60 | 61 | if self.formats.csv: 62 | frame.to_csv(self.path / (self.name + ".csv"), index=False) 63 | 64 | if self.formats.jsonl: 65 | frame.to_json( 66 | self.path / (self.name + ".jsonl"), 67 | index=False, 68 | lines=True, 69 | orient="records", 70 | force_ascii=False, 71 | ) 72 | 73 | if self.formats.xlsx: 74 | frame.to_excel( 75 | self.path / (self.name + ".xlsx"), index=False, sheet_name=self.title 76 | ) 77 | -------------------------------------------------------------------------------- /roktracker/utils/general.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | from os import PathLike 4 | import random 5 | import string 6 | import time 7 | import cv2 8 | import numpy as np 9 | 10 | from typing import Any 11 | from cv2.typing import MatLike 12 | 13 | from dummy_root import get_app_root 14 | from roktracker.utils.exceptions import ConfigError 15 | 16 | 17 | def load_config(): 18 | try: 19 | with open(get_app_root() / "config.json", "rt") as config_file: 20 | return json.load(config_file) 21 | except json.JSONDecodeError as e: 22 | if e.msg == "Invalid \\escape": 23 | raise ConfigError( 24 | f"Config is invalid. Make sure you use \\\\ instead of \\. The error happened in line {e.lineno}." 25 | ) 26 | if e.msg == "Invalid control character at": 27 | raise ConfigError( 28 | f"Config is invalid. {e.msg} char {e.colno} in line {e.lineno}." 29 | ) 30 | raise ConfigError(f"Config is invalid. {e.msg} in line {e.lineno}.") 31 | except FileNotFoundError: 32 | raise ConfigError( 33 | "Config file is missing: make sure config.json is in the same folder as your scanner." 34 | ) 35 | 36 | 37 | def to_int_check(element) -> int: 38 | try: 39 | return int(element) 40 | except ValueError: 41 | # return dummy 0 42 | return int(0) 43 | 44 | 45 | def to_int_or(element: Any, fallback: int) -> int: 46 | try: 47 | return int(element) 48 | except ValueError: 49 | # return dummy 0 50 | return int(fallback) 51 | 52 | 53 | def is_string_int(element: str, allow_empty=False) -> bool: 54 | if allow_empty and element == "": 55 | return True 56 | 57 | try: 58 | _ = int(element) 59 | return True 60 | except ValueError: 61 | return False 62 | 63 | 64 | def is_string_float(element: str, allow_empty=False) -> bool: 65 | if allow_empty and element == "": 66 | return True 67 | 68 | try: 69 | _ = float(element) 70 | return True 71 | except ValueError: 72 | return False 73 | 74 | 75 | def generate_random_id(length: int) -> str: 76 | alphabet = string.ascii_lowercase + string.digits 77 | return "".join(random.choices(alphabet, k=length)) 78 | 79 | 80 | def next_alpha(s: str) -> str: 81 | return chr((ord(s.upper()) + 1 - 65) % 26 + 65) 82 | 83 | 84 | def random_delay() -> float: 85 | return random.random() * 0.1 86 | 87 | 88 | def wait_random_range(min_time: float, max_offset: float) -> None: 89 | time.sleep(random.uniform(min_time, min_time + max_offset)) 90 | 91 | 92 | def format_timedelta_to_HHMMSS(td: datetime.timedelta) -> str: 93 | td_in_seconds = td.total_seconds() 94 | hours, remainder = divmod(td_in_seconds, 3600) 95 | minutes, seconds = divmod(remainder, 60) 96 | hours = int(hours) 97 | minutes = int(minutes) 98 | seconds = int(seconds) 99 | if minutes < 10: 100 | minutes = "0{}".format(minutes) 101 | if seconds < 10: 102 | seconds = "0{}".format(seconds) 103 | return "{}:{}:{}".format(hours, minutes, seconds) 104 | 105 | 106 | # This workaroud is needed because cv2 doesn't support UTF-8 paths 107 | def load_cv2_img(path: str | PathLike[Any], flags: int) -> MatLike: 108 | return cv2.imdecode( 109 | np.fromfile(file=path, dtype=np.uint8), 110 | flags, 111 | ) 112 | 113 | 114 | # This workaroud is needed because cv2 doesn't support UTF-8 paths 115 | def write_cv2_img(img: MatLike, path: str | PathLike[Any], filetype: str) -> None: 116 | is_success, im_buf_arr = cv2.imencode("." + filetype, img) 117 | 118 | if is_success: 119 | im_buf_arr.tofile(path) 120 | -------------------------------------------------------------------------------- /roktracker/utils/validator.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import os 3 | import sys 4 | import glob 5 | import logging 6 | from pathlib import Path 7 | from typing import List 8 | from dummy_root import get_app_root 9 | from roktracker.utils.console import console 10 | from pathvalidate import sanitize_filename, ValidationError, validate_filename 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | @dataclass 16 | class ValidationResult: 17 | success: bool 18 | messages: List[str] 19 | 20 | 21 | @dataclass 22 | class SanitizationResult: 23 | valid: bool 24 | messages: List[str] 25 | result: str 26 | 27 | 28 | def validate_installation() -> ValidationResult: 29 | result: List[str] = [] 30 | root_dir = get_app_root() 31 | 32 | tess_dir = root_dir / "deps" / "tessdata" 33 | adb_dir = root_dir / "deps" / "platform-tools" 34 | 35 | tessdata_present = False 36 | adb_present = False 37 | 38 | if os.path.exists(tess_dir): 39 | tessdata_present = True 40 | available_trainingdata = glob.glob(str(tess_dir / "*.traineddata")) 41 | if len(available_trainingdata) == 0: 42 | title = "Tess dir found, but no training data is present" 43 | result.append(title) 44 | console.log(title) 45 | logger.critical(title) 46 | 47 | message = f"It is expected that you put the training files for tesseract in this folder: {tess_dir}" 48 | result.append(message) 49 | console.log(message) 50 | logger.info(message) 51 | tessdata_present = False 52 | else: 53 | title = "Tess dir is missing" 54 | result.append(title) 55 | console.log(title) 56 | logger.critical(title) 57 | 58 | message = f"It is expected that you create the folder ({tess_dir}) and put the training files for tesseract in it." 59 | result.append(message) 60 | console.log(message) 61 | logger.info(message) 62 | tessdata_present = False 63 | 64 | if os.path.exists(adb_dir): 65 | adb_present = True 66 | if not os.path.isfile(adb_dir / "adb.exe"): 67 | title = "Adb dir found, but adb.exe missing" 68 | result.append(title) 69 | console.log(title) 70 | logger.critical(title) 71 | 72 | message = f"It is expected that your adb.exe file is located in this folder: {adb_dir}" 73 | result.append(message) 74 | console.log(message) 75 | logger.info(message) 76 | adb_present = False 77 | else: 78 | title = "Adb dir is missing" 79 | result.append(title) 80 | console.log(title) 81 | logger.critical(title) 82 | 83 | message = f"It is expected that you create the folder ({adb_dir}) and put extract the downloaded platform tools into it." 84 | result.append(message) 85 | console.log(message) 86 | logger.info(message) 87 | adb_present = False 88 | 89 | return ValidationResult(tessdata_present and adb_present, result) 90 | 91 | 92 | def sanitize_scanname(filename: str) -> SanitizationResult: 93 | if filename == "": 94 | return SanitizationResult(True, [], "") 95 | 96 | valid = True 97 | result = "" 98 | errors: List[str] = [] 99 | 100 | try: 101 | validate_filename(filename) 102 | except ValidationError as e: 103 | valid = False 104 | message = f"Scan name validatation error: {e}" 105 | errors.append(message) 106 | console.log(message) 107 | logger.info(message) 108 | 109 | try: 110 | result = str(sanitize_filename(filename)) 111 | except ValidationError as e: 112 | valid = False 113 | message = f"Scan name validatation error: {e}" 114 | errors.append(message) 115 | console.log(message) 116 | logger.info(message) 117 | 118 | return SanitizationResult(valid, errors, result) 119 | -------------------------------------------------------------------------------- /roktracker/alliance/pandas_handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from os import PathLike 3 | from typing import Any, List 4 | import pandas as pd 5 | import pathlib 6 | 7 | from roktracker.alliance.governor_data import GovernorData 8 | from roktracker.utils.general import to_int_or 9 | from roktracker.utils.output_formats import OutputFormats 10 | from datetime import date 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class PandasHandler: 16 | def __init__( 17 | self, 18 | path: str | PathLike[Any], 19 | filename: str, 20 | formats: OutputFormats, 21 | title: str = str(date.today()), 22 | ): 23 | self.title = title 24 | self.path = pathlib.Path(path) 25 | self.name = filename 26 | self.formats = formats 27 | self.data_list = [] 28 | self.last_score = -2 29 | 30 | def write_governors(self, gov_data: List[GovernorData]) -> bool: 31 | reached_bottom = False 32 | 33 | for gov in gov_data: 34 | if not self.is_duplicate(gov): 35 | int_score = to_int_or(gov.score, -1) 36 | if self.last_score == -2 and int_score != -1: 37 | self.last_score = int_score 38 | elif int_score == -1: 39 | logger.warning( 40 | f"Error in score detected (score not readable) at rank {len(self.data_list)}" 41 | ) 42 | int_score = self.last_score 43 | elif int_score > self.last_score: 44 | logger.warning( 45 | f"Error in score detected (score too high) at rank {len(self.data_list)}" 46 | ) 47 | int_score = self.last_score 48 | else: 49 | self.last_score = int_score 50 | 51 | # Insert gov if not duplicate 52 | self.data_list.append( 53 | { 54 | "Image": gov.img_path, 55 | "Name": gov.name, 56 | "Score": int_score, 57 | } 58 | ) 59 | else: 60 | # Duplicated govs can only happen on last page 61 | reached_bottom = True 62 | 63 | return reached_bottom 64 | 65 | def is_duplicate(self, governor: GovernorData) -> bool: 66 | if len(self.data_list) == 0: 67 | return False 68 | 69 | # look at the last 5 governors 70 | for data in self.data_list[-min(len(self.data_list), 5) :]: 71 | if data["Name"] == governor.name and data["Score"] == to_int_or( 72 | governor.score, -1 73 | ): 74 | return True 75 | 76 | return False 77 | 78 | def save(self, trimm_to=0, sum_total=False): 79 | frame = pd.DataFrame(self.data_list) 80 | # do trimming 81 | if trimm_to > 0: 82 | frame = frame.head(trimm_to) 83 | 84 | # strip the image path 85 | frame_stripped = frame.drop("Image", axis=1) 86 | frame_stripped = frame_stripped.set_index("Name", drop=False) 87 | 88 | # add sum 89 | if sum_total: 90 | frame_stripped.loc["Total"] = frame_stripped.sum(numeric_only=True, axis=0) 91 | frame_stripped.at["Total", "Name"] = "Total" 92 | 93 | if self.formats.csv: 94 | frame_stripped.to_csv(self.path / (self.name + ".csv"), index=False) 95 | 96 | if self.formats.jsonl: 97 | frame_stripped.to_json( 98 | self.path / (self.name + ".jsonl"), 99 | index=False, 100 | lines=True, 101 | orient="records", 102 | force_ascii=False, 103 | ) 104 | 105 | if self.formats.xlsx: 106 | with pd.ExcelWriter( 107 | self.path / (self.name + ".xlsx"), engine="xlsxwriter" 108 | ) as writer: 109 | frame_stripped.to_excel( 110 | writer, index=False, sheet_name=self.title, startcol=1 111 | ) 112 | 113 | worksheet = writer.sheets[self.title] 114 | worksheet.set_default_row(24.75) 115 | worksheet.set_column(0, 0, 42) 116 | for index, row in frame.iterrows(): 117 | excel_row = int(index.__str__()) + 2 118 | worksheet.insert_image( 119 | f"A{excel_row}", row["Image"], {"y_offset": 5} 120 | ) 121 | -------------------------------------------------------------------------------- /roktracker/utils/gui.py: -------------------------------------------------------------------------------- 1 | from tkinter import CENTER 2 | import customtkinter 3 | 4 | 5 | # Modification of the CTkInputDialog class 6 | class InfoDialog(customtkinter.CTkToplevel): 7 | def __init__( 8 | self, 9 | display_title, 10 | display_text, 11 | size="300x400", 12 | close_cb=lambda: None, 13 | *args, 14 | **kwargs 15 | ): 16 | super().__init__(*args, **kwargs) 17 | 18 | self._running: bool = False 19 | self._title = display_title 20 | self._text = display_text 21 | self.close_cb = close_cb 22 | 23 | self.title(self._title) 24 | self.geometry(size) 25 | self.lift() # lift window on top 26 | self.attributes("-topmost", True) # stay on top 27 | self.protocol("WM_DELETE_WINDOW", self._on_closing) 28 | self.after( 29 | 10, self._create_widgets 30 | ) # create widgets with slight delay, to avoid white flickering of background 31 | self.resizable(False, False) 32 | self.grab_set() # make other windows not clickable 33 | 34 | def _create_widgets(self): 35 | self.grid_columnconfigure((0, 1), weight=1) # type: ignore 36 | self.rowconfigure(0, weight=1) 37 | 38 | self._label = customtkinter.CTkLabel( 39 | master=self, 40 | width=300, 41 | fg_color="transparent", 42 | text=self._text, 43 | justify="center", 44 | ) 45 | self.bind( 46 | "", 47 | lambda e: self._label.configure(wraplength=self._label.winfo_width()), 48 | ) 49 | self._label.grid(row=0, column=0, columnspan=1, padx=20, pady=20, sticky="ew") 50 | 51 | self._ok_button = customtkinter.CTkButton( 52 | master=self, 53 | border_width=0, 54 | text="Ok", 55 | command=self._ok_event, 56 | ) 57 | self._ok_button.grid( 58 | row=1, column=0, columnspan=1, padx=(20, 10), pady=(0, 20), sticky="ew" 59 | ) 60 | 61 | def _ok_event(self, event=None): 62 | self.grab_release() 63 | self.destroy() 64 | self.close_cb() 65 | 66 | def _on_closing(self): 67 | self.grab_release() 68 | self.destroy() 69 | self.close_cb() 70 | 71 | def wait_for_close(self): 72 | self.master.wait_window(self) 73 | 74 | 75 | # Modification of the CTkInputDialog class 76 | class ConfirmDialog(customtkinter.CTkToplevel): 77 | def __init__(self, display_title, display_text, size="300x400", *args, **kwargs): 78 | super().__init__(*args, **kwargs) 79 | 80 | self._user_input: bool = False 81 | self._running: bool = False 82 | self._title = display_title 83 | self._text = display_text 84 | 85 | self.title(self._title) 86 | self.geometry(size) 87 | self.lift() # lift window on top 88 | self.attributes("-topmost", True) # stay on top 89 | self.protocol("WM_DELETE_WINDOW", self._on_closing) 90 | self.after( 91 | 10, self._create_widgets 92 | ) # create widgets with slight delay, to avoid white flickering of background 93 | self.resizable(False, False) 94 | self.grab_set() # make other windows not clickable 95 | 96 | def _create_widgets(self): 97 | self.grid_columnconfigure((0, 1), weight=1) # type: ignore 98 | self.rowconfigure(0, weight=1) 99 | 100 | self._label = customtkinter.CTkLabel( 101 | master=self, 102 | width=300, 103 | wraplength=300, 104 | fg_color="transparent", 105 | text=self._text, 106 | ) 107 | self._label.grid(row=0, column=0, columnspan=2, padx=20, pady=20, sticky="ew") 108 | 109 | self._ok_button = customtkinter.CTkButton( 110 | master=self, 111 | width=100, 112 | border_width=0, 113 | text="Yes", 114 | command=self._ok_event, 115 | ) 116 | self._ok_button.grid( 117 | row=1, 118 | column=0, 119 | columnspan=1, 120 | padx=(20, 10), 121 | pady=(0, 20), 122 | sticky="ew", 123 | ) 124 | 125 | self._cancel_button = customtkinter.CTkButton( 126 | master=self, 127 | width=100, 128 | border_width=0, 129 | text="No", 130 | command=self._cancel_event, 131 | ) 132 | self._cancel_button.grid( 133 | row=1, 134 | column=1, 135 | columnspan=1, 136 | padx=(10, 20), 137 | pady=(0, 20), 138 | sticky="ew", 139 | ) 140 | 141 | def _ok_event(self, event=None): 142 | self._user_input = True 143 | self.grab_release() 144 | self.destroy() 145 | 146 | def _on_closing(self): 147 | self.grab_release() 148 | self.destroy() 149 | 150 | def _cancel_event(self): 151 | self._user_input = False 152 | self.grab_release() 153 | self.destroy() 154 | 155 | def get_input(self): 156 | self.master.wait_window(self) 157 | return self._user_input 158 | -------------------------------------------------------------------------------- /roktracker/utils/adb.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from roktracker.utils.console import console 3 | from PIL.Image import Image, new as NewImage 4 | from com.dtmilano.android.adb.adbclient import AdbClient 5 | from pathlib import Path 6 | import subprocess 7 | import socket 8 | import configparser 9 | import sys 10 | 11 | from roktracker.utils.exceptions import AdbError 12 | from roktracker.utils.general import to_int_or 13 | 14 | 15 | def get_bluestacks_port(bluestacks_device_name: str, config) -> int: 16 | default_port = to_int_or(config["general"]["adb_port"], 5555) 17 | # try to read port from bluestacks config 18 | if config["general"]["emulator"] == "bluestacks": 19 | try: 20 | dummy = "AmazingDummy" 21 | with open(config["general"]["bluestacks"]["config"], "r") as config_file: 22 | file_content = "[" + dummy + "]\n" + config_file.read() 23 | bluestacks_config = configparser.RawConfigParser() 24 | bluestacks_config.read_string(file_content) 25 | 26 | for key, value in bluestacks_config.items(dummy): 27 | if value == f'"{bluestacks_device_name}"': 28 | key_port = key.replace("display_name", "status.adb_port") 29 | port = bluestacks_config.get(dummy, key_port) 30 | return int(port.strip('"')) 31 | except: 32 | console.print( 33 | "[red]Could not parse or find bluestacks config. Defaulting to 5555.[/red]" 34 | ) 35 | return default_port 36 | 37 | 38 | class AdvancedAdbClient: 39 | def __init__( 40 | self, 41 | adb_path: str, 42 | port: int, 43 | player: str, 44 | script_base: str | Path, 45 | start_immediately=False, 46 | ): 47 | self.server_port = 0 48 | self.client_port = port 49 | self.adb_path = adb_path 50 | self.started = start_immediately 51 | self.player = player 52 | self.script_base = Path(script_base) 53 | 54 | if start_immediately: 55 | self.start_adb() 56 | 57 | def get_free_port(self) -> int: 58 | s = socket.socket() 59 | s.bind(("", 0)) 60 | port = s.getsockname()[1] 61 | s.close() 62 | return port 63 | 64 | def set_adb_path(self, path: str) -> None: 65 | self.adb_path = path 66 | 67 | def kill_adb(self) -> None: 68 | console.print("Killing ADB server...") 69 | process = subprocess.run( 70 | [self.adb_path, "-P " + str(self.server_port), "kill-server"], 71 | stdout=subprocess.PIPE, 72 | universal_newlines=True, 73 | ) 74 | console.print(process.stdout) 75 | 76 | def start_adb(self) -> None: 77 | self.server_port = self.get_free_port() 78 | console.print("Starting adb server and connecting to adb device...") 79 | process = subprocess.run( 80 | [ 81 | self.adb_path, 82 | "-P " + str(self.server_port), 83 | "connect", 84 | "localhost:" + str(self.client_port), 85 | ], 86 | stdout=subprocess.PIPE, 87 | universal_newlines=True, 88 | ) 89 | console.print(process.stdout) 90 | try: 91 | adb_client = AdbClient( 92 | serialno=".*", hostname="localhost", port=self.server_port 93 | ) 94 | except RuntimeError as error: 95 | console.log("No device connected, aborting.") 96 | self.kill_adb() 97 | raise AdbError(str(error)) 98 | self.device = adb_client 99 | 100 | def secure_adb_shell(self, command_to_execute: str) -> str: 101 | result = "" 102 | for i in range(3): 103 | try: 104 | result = str(self.device.shell(command_to_execute)) 105 | except: 106 | console.print("[red]ADB crashed[/red]") 107 | self.kill_adb() 108 | self.start_adb() 109 | else: 110 | return result 111 | return result 112 | 113 | def secure_adb_tap(self, position: Tuple[int, int]): 114 | self.secure_adb_shell(f"input tap {position[0]} {position[1]}") 115 | 116 | def secure_adb_screencap(self) -> Image: 117 | result = NewImage(mode="RGB", size=(1, 1)) 118 | for i in range(3): 119 | try: 120 | result = self.device.takeSnapshot(reconnect=True) 121 | except: 122 | console.print("[red]ADB crashed[/red]") 123 | self.kill_adb() 124 | self.start_adb() 125 | else: 126 | return result 127 | return result 128 | 129 | def adb_send_events(self, input_device_name: str, event_file: str | Path) -> None: 130 | if input_device_name == "Touch": 131 | if self.player == "ld": 132 | input_device_name = "ABS_MT_POSITION_Y" 133 | 134 | idn = self.secure_adb_shell( 135 | f"getevent -pl 2>&1 | sed -n '/^add/{{h}}/{input_device_name}/{{x;s/[^/]*//p}}'", 136 | ) 137 | idn = str(idn).strip() 138 | macroFile = open(self.script_base / self.player / event_file, "r") 139 | lines = macroFile.readlines() 140 | 141 | for line in lines: 142 | self.secure_adb_shell(f"""sendevent {idn} {line.strip()}""") 143 | -------------------------------------------------------------------------------- /roktracker/kingdom/governor_data.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from dataclasses import dataclass 4 | from roktracker.utils.general import is_string_int, to_int_check 5 | 6 | 7 | @dataclass 8 | class GovernorData: 9 | id: str = "Skipped" 10 | name: str = "Skipped" 11 | power: str = "Skipped" 12 | killpoints: str = "Skipped" 13 | alliance: str = "Skipped" 14 | t1_kills: str = "Skipped" 15 | t1_kp: str = "Skipped" 16 | t2_kills: str = "Skipped" 17 | t2_kp: str = "Skipped" 18 | t3_kills: str = "Skipped" 19 | t3_kp: str = "Skipped" 20 | t4_kills: str = "Skipped" 21 | t4_kp: str = "Skipped" 22 | t5_kills: str = "Skipped" 23 | t5_kp: str = "Skipped" 24 | ranged_points: str = "Skipped" 25 | dead: str = "Skipped" 26 | rss_assistance: str = "Skipped" 27 | rss_gathered: str = "Skipped" 28 | helps: str = "Skipped" 29 | 30 | def t45_kills(self) -> str: 31 | if self.t4_kills != "Skipped" and self.t5_kills != "Skipped": 32 | return str(to_int_check(self.t4_kills) + to_int_check(self.t5_kills)) 33 | else: 34 | return "Skipped" 35 | 36 | def total_kills(self) -> str: 37 | if ( 38 | self.t1_kills != "Skipped" 39 | and self.t2_kills != "Skipped" 40 | and self.t3_kills != "Skipped" 41 | and self.t4_kills != "Skipped" 42 | and self.t5_kills != "Skipped" 43 | ): 44 | return str( 45 | to_int_check(self.t1_kills) 46 | + to_int_check(self.t2_kills) 47 | + to_int_check(self.t3_kills) 48 | + to_int_check(self.t45_kills()) 49 | ) 50 | else: 51 | return "Skipped" 52 | 53 | def flag_unknown(self): 54 | if self.power == "": 55 | self.power = "Unknown" 56 | 57 | if self.killpoints == "": 58 | self.killpoints = "Unknown" 59 | 60 | if self.t1_kills == "": 61 | self.t1_kills = "Unknown" 62 | 63 | if self.t2_kills == "": 64 | self.t2_kills = "Unknown" 65 | 66 | if self.t3_kills == "": 67 | self.t3_kills = "Unknown" 68 | 69 | if self.t4_kills == "": 70 | self.t4_kills = "Unknown" 71 | 72 | if self.t5_kills == "": 73 | self.t5_kills = "Unknown" 74 | 75 | if self.t1_kp == "": 76 | self.t1_kp = "Unknown" 77 | 78 | if self.t2_kp == "": 79 | self.t2_kp = "Unknown" 80 | 81 | if self.t3_kp == "": 82 | self.t3_kp = "Unknown" 83 | 84 | if self.t4_kp == "": 85 | self.t4_kp = "Unknown" 86 | 87 | if self.t5_kp == "": 88 | self.t5_kp = "Unknown" 89 | 90 | if self.ranged_points == "": 91 | self.ranged_points = "Unknown" 92 | 93 | if self.dead == "": 94 | self.dead = "Unknown" 95 | 96 | if self.rss_gathered == "": 97 | self.rss_gathered = "Unknown" 98 | 99 | if self.rss_assistance == "": 100 | self.rss_assistance = "Unknown" 101 | 102 | if self.helps == "": 103 | self.helps = "Unknown" 104 | 105 | @staticmethod 106 | def intify_value(value: str) -> int: 107 | if value == "Unknown": 108 | return -1 109 | elif value == "Skipped": 110 | return -2 111 | elif not is_string_int(value): 112 | return -3 113 | else: 114 | return int(value) 115 | 116 | def validate_kills(self) -> bool: 117 | expectedKp = ( 118 | math.floor(to_int_check(self.t1_kills) * 0.2) 119 | + (to_int_check(self.t2_kills) * 2) 120 | + (to_int_check(self.t3_kills) * 4) 121 | + (to_int_check(self.t4_kills) * 10) 122 | + (to_int_check(self.t5_kills) * 20) 123 | ) 124 | 125 | return to_int_check(self.killpoints) == expectedKp 126 | 127 | def validate_killpoints(self) -> bool: 128 | expectedKp = ( 129 | to_int_check(self.t1_kp) 130 | + to_int_check(self.t2_kp) 131 | + to_int_check(self.t3_kp) 132 | + to_int_check(self.t4_kp) 133 | + to_int_check(self.t5_kp) 134 | ) 135 | return to_int_check(self.killpoints) == expectedKp 136 | 137 | def reconstruct_kills(self) -> bool: 138 | # Kills can only reconstructed if kp are correct 139 | if self.validate_killpoints(): 140 | kills_t1 = to_int_check(self.t1_kp) / 0.2 141 | 142 | if ( 143 | kills_t1 < to_int_check(self.t1_kp) <= (kills_t1 + 4) 144 | ): # fix t1 kills if no error is present 145 | kills_t1 = to_int_check(self.t1_kp) 146 | elif ( 147 | kills_t1 % 10 < to_int_check(self.t1_kp) % 10 <= (kills_t1 + 4) % 10 148 | ): # try to fix t1 with error (assuming last digit is correct) 149 | kill_dif_t1 = (to_int_check(self.t1_kp) % 10) - (kills_t1 % 10) 150 | kills_t1 = kills_t1 + kill_dif_t1 151 | 152 | self.t1_kills = str(kills_t1) 153 | self.t2_kills = str(to_int_check(self.t2_kp) / 2) 154 | self.t3_kills = str(to_int_check(self.t3_kp) / 4) 155 | self.t4_kills = str(to_int_check(self.t4_kp) / 10) 156 | self.t5_kills = str(to_int_check(self.t5_kp) / 20) 157 | return True 158 | else: 159 | return False 160 | -------------------------------------------------------------------------------- /honor_scanner_console.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from dummy_root import get_app_root 4 | from roktracker.utils.check_python import check_py_version 5 | from roktracker.utils.exception_handling import ConsoleExceptionHander 6 | from roktracker.utils.output_formats import OutputFormats 7 | 8 | logging.basicConfig( 9 | filename=str(get_app_root() / "honor-scanner.log"), 10 | encoding="utf-8", 11 | format="%(asctime)s %(module)s %(levelname)s %(message)s", 12 | level=logging.INFO, 13 | datefmt="%Y-%m-%d %H:%M:%S", 14 | ) 15 | 16 | check_py_version((3, 11)) 17 | 18 | import json 19 | import questionary 20 | import signal 21 | import sys 22 | 23 | 24 | from roktracker.alliance.batch_printer import print_batch 25 | from roktracker.honor.scanner import HonorScanner 26 | from roktracker.utils.adb import * 27 | from roktracker.utils.console import console 28 | from roktracker.utils.general import * 29 | from roktracker.utils.ocr import get_supported_langs 30 | from roktracker.utils.validator import sanitize_scanname, validate_installation 31 | 32 | 33 | logger = logging.getLogger(__name__) 34 | ex_handler = ConsoleExceptionHander(logger) 35 | 36 | sys.excepthook = ex_handler.handle_exception 37 | threading.excepthook = ex_handler.handle_thread_exception 38 | 39 | 40 | def handle_exception(exc_type, exc_value, exc_traceback): 41 | if issubclass(exc_type, KeyboardInterrupt): 42 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 43 | return 44 | 45 | logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) 46 | 47 | 48 | sys.excepthook = handle_exception 49 | 50 | 51 | def ask_abort(honor_scanner: HonorScanner) -> None: 52 | stop = questionary.confirm( 53 | message="Do you want to stop the scanner?:", auto_enter=False, default=False 54 | ).ask() 55 | 56 | if stop: 57 | console.print("Scan will aborted after next governor.") 58 | honor_scanner.end_scan() 59 | 60 | 61 | def main(): 62 | if not validate_installation().success: 63 | sys.exit(2) 64 | 65 | root_dir = get_app_root() 66 | 67 | try: 68 | config = load_config() 69 | except ConfigError as e: 70 | logger.fatal(str(e)) 71 | console.log(str(e)) 72 | sys.exit(3) 73 | 74 | console.print( 75 | "Tesseract languages available: " 76 | + get_supported_langs(str(root_dir / "deps" / "tessdata")) 77 | ) 78 | 79 | try: 80 | bluestacks_device_name = questionary.text( 81 | message="Name of your bluestacks instance:", 82 | default=config["general"]["bluestacks"]["name"], 83 | ).unsafe_ask() 84 | 85 | bluestacks_port = int( 86 | questionary.text( 87 | f"Adb port of device (detected {get_bluestacks_port(bluestacks_device_name, config)}):", 88 | default=str(get_bluestacks_port(bluestacks_device_name, config)), 89 | validate=lambda port: is_string_int(port), 90 | ).unsafe_ask() 91 | ) 92 | 93 | kingdom = questionary.text( 94 | message="Kingdom name (used for file name):", 95 | default=config["scan"]["kingdom_name"], 96 | ).unsafe_ask() 97 | 98 | validated_name = sanitize_scanname(kingdom) 99 | while not validated_name.valid: 100 | kingdom = questionary.text( 101 | message="Kingdom name (Previous name was invalid):", 102 | default=validated_name.result, 103 | ).unsafe_ask() 104 | validated_name = sanitize_scanname(kingdom) 105 | 106 | scan_amount = int( 107 | questionary.text( 108 | message="Number of people to scan:", 109 | validate=lambda port: is_string_int(port), 110 | default=str(config["scan"]["people_to_scan"]), 111 | ).unsafe_ask() 112 | ) 113 | 114 | save_formats = OutputFormats() 115 | save_formats_tmp = questionary.checkbox( 116 | "In what format should the result be saved?", 117 | choices=[ 118 | questionary.Choice( 119 | "Excel (xlsx)", 120 | value="xlsx", 121 | checked=config["scan"]["formats"]["xlsx"], 122 | ), 123 | questionary.Choice( 124 | "Comma seperated values (csv)", 125 | value="csv", 126 | checked=config["scan"]["formats"]["csv"], 127 | ), 128 | questionary.Choice( 129 | "JSON Lines (jsonl)", 130 | value="jsonl", 131 | checked=config["scan"]["formats"]["jsonl"], 132 | ), 133 | ], 134 | ).unsafe_ask() 135 | 136 | if save_formats_tmp == [] or save_formats_tmp == None: 137 | console.print("Exiting, no formats selected.") 138 | return 139 | else: 140 | save_formats.from_list(save_formats_tmp) 141 | except: 142 | console.log("User abort. Exiting scanner.") 143 | sys.exit(3) 144 | 145 | try: 146 | honor_scanner = HonorScanner(bluestacks_port, config) 147 | honor_scanner.set_batch_callback(print_batch) 148 | 149 | console.print( 150 | f"The UUID of this scan is [green]{honor_scanner.run_id}[/green]", 151 | highlight=False, 152 | ) 153 | signal.signal(signal.SIGINT, lambda _, __: ask_abort(honor_scanner)) 154 | 155 | honor_scanner.start_scan(kingdom, scan_amount, save_formats) 156 | except AdbError as error: 157 | logger.error( 158 | "An error with the adb connection occured (probably wrong port). Exact message: " 159 | + str(error) 160 | ) 161 | console.print( 162 | "An error with the adb connection occured. Please verfiy that you use the correct port.\nExact message: " 163 | + str(error) 164 | ) 165 | 166 | 167 | if __name__ == "__main__": 168 | main() 169 | input("Press enter to exit...") 170 | -------------------------------------------------------------------------------- /seed_scanner_console.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from dummy_root import get_app_root 4 | from roktracker.utils.check_python import check_py_version 5 | from roktracker.utils.exception_handling import ConsoleExceptionHander 6 | from roktracker.utils.output_formats import OutputFormats 7 | 8 | logging.basicConfig( 9 | filename=str(get_app_root() / "seed-scanner.log"), 10 | encoding="utf-8", 11 | format="%(asctime)s %(module)s %(levelname)s %(message)s", 12 | level=logging.INFO, 13 | datefmt="%Y-%m-%d %H:%M:%S", 14 | ) 15 | 16 | check_py_version((3, 11)) 17 | 18 | import json 19 | import questionary 20 | import signal 21 | import sys 22 | 23 | 24 | from roktracker.alliance.batch_printer import print_batch 25 | from roktracker.seed.scanner import SeedScanner 26 | from roktracker.utils.adb import * 27 | from roktracker.utils.console import console 28 | from roktracker.utils.general import * 29 | from roktracker.utils.ocr import get_supported_langs 30 | from roktracker.utils.validator import sanitize_scanname, validate_installation 31 | 32 | 33 | logger = logging.getLogger(__name__) 34 | ex_handler = ConsoleExceptionHander(logger) 35 | 36 | 37 | sys.excepthook = ex_handler.handle_exception 38 | threading.excepthook = ex_handler.handle_thread_exception 39 | 40 | 41 | def handle_exception(exc_type, exc_value, exc_traceback): 42 | if issubclass(exc_type, KeyboardInterrupt): 43 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 44 | return 45 | 46 | logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) 47 | 48 | 49 | sys.excepthook = handle_exception 50 | 51 | 52 | def ask_abort(alliance_scanner: SeedScanner) -> None: 53 | stop = questionary.confirm( 54 | message="Do you want to stop the scanner?:", auto_enter=False, default=False 55 | ).ask() 56 | 57 | if stop: 58 | console.print("Scan will aborted after next governor.") 59 | alliance_scanner.end_scan() 60 | 61 | 62 | def main(): 63 | if not validate_installation().success: 64 | sys.exit(2) 65 | 66 | root_dir = get_app_root() 67 | 68 | try: 69 | config = load_config() 70 | except ConfigError as e: 71 | logger.fatal(str(e)) 72 | console.log(str(e)) 73 | sys.exit(3) 74 | 75 | console.print( 76 | "Tesseract languages available: " 77 | + get_supported_langs(str(root_dir / "deps" / "tessdata")) 78 | ) 79 | 80 | try: 81 | bluestacks_device_name = questionary.text( 82 | message="Name of your bluestacks instance:", 83 | default=config["general"]["bluestacks"]["name"], 84 | ).unsafe_ask() 85 | 86 | bluestacks_port = int( 87 | questionary.text( 88 | f"Adb port of device (detected {get_bluestacks_port(bluestacks_device_name, config)}):", 89 | default=str(get_bluestacks_port(bluestacks_device_name, config)), 90 | validate=lambda port: is_string_int(port), 91 | ).unsafe_ask() 92 | ) 93 | 94 | kingdom = questionary.text( 95 | message="Alliance name (used for file name):", 96 | default=config["scan"]["kingdom_name"], 97 | ).unsafe_ask() 98 | 99 | validated_name = sanitize_scanname(kingdom) 100 | while not validated_name.valid: 101 | kingdom = questionary.text( 102 | message="Alliance name (Previous name was invalid):", 103 | default=validated_name.result, 104 | ).unsafe_ask() 105 | validated_name = sanitize_scanname(kingdom) 106 | 107 | scan_amount = int( 108 | questionary.text( 109 | message="Number of people to scan:", 110 | validate=lambda port: is_string_int(port), 111 | default=str(config["scan"]["people_to_scan"]), 112 | ).unsafe_ask() 113 | ) 114 | 115 | save_formats = OutputFormats() 116 | save_formats_tmp = questionary.checkbox( 117 | "In what format should the result be saved?", 118 | choices=[ 119 | questionary.Choice( 120 | "Excel (xlsx)", 121 | value="xlsx", 122 | checked=config["scan"]["formats"]["xlsx"], 123 | ), 124 | questionary.Choice( 125 | "Comma seperated values (csv)", 126 | value="csv", 127 | checked=config["scan"]["formats"]["csv"], 128 | ), 129 | questionary.Choice( 130 | "JSON Lines (jsonl)", 131 | value="jsonl", 132 | checked=config["scan"]["formats"]["jsonl"], 133 | ), 134 | ], 135 | ).unsafe_ask() 136 | 137 | if save_formats_tmp == [] or save_formats_tmp == None: 138 | console.print("Exiting, no formats selected.") 139 | return 140 | else: 141 | save_formats.from_list(save_formats_tmp) 142 | except: 143 | console.log("User abort. Exiting scanner.") 144 | sys.exit(3) 145 | 146 | try: 147 | alliance_scanner = SeedScanner(bluestacks_port, config) 148 | alliance_scanner.set_batch_callback(print_batch) 149 | 150 | console.print( 151 | f"The UUID of this scan is [green]{alliance_scanner.run_id}[/green]", 152 | highlight=False, 153 | ) 154 | signal.signal(signal.SIGINT, lambda _, __: ask_abort(alliance_scanner)) 155 | 156 | alliance_scanner.start_scan(kingdom, scan_amount, save_formats) 157 | except AdbError as error: 158 | logger.error( 159 | "An error with the adb connection occured (probably wrong port). Exact message: " 160 | + str(error) 161 | ) 162 | console.print( 163 | "An error with the adb connection occured. Please verfiy that you use the correct port.\nExact message: " 164 | + str(error) 165 | ) 166 | 167 | 168 | if __name__ == "__main__": 169 | main() 170 | input("Press enter to exit...") 171 | -------------------------------------------------------------------------------- /alliance_scanner_console.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from dummy_root import get_app_root 4 | from roktracker.utils.check_python import check_py_version 5 | from roktracker.utils.exception_handling import ConsoleExceptionHander 6 | from roktracker.utils.output_formats import OutputFormats 7 | 8 | logging.basicConfig( 9 | filename=str(get_app_root() / "alliance-scanner.log"), 10 | encoding="utf-8", 11 | format="%(asctime)s %(module)s %(levelname)s %(message)s", 12 | level=logging.INFO, 13 | datefmt="%Y-%m-%d %H:%M:%S", 14 | ) 15 | 16 | check_py_version((3, 11)) 17 | 18 | import json 19 | import questionary 20 | import signal 21 | import sys 22 | 23 | 24 | from roktracker.alliance.batch_printer import print_batch 25 | from roktracker.alliance.scanner import AllianceScanner 26 | from roktracker.utils.adb import * 27 | from roktracker.utils.console import console 28 | from roktracker.utils.general import * 29 | from roktracker.utils.ocr import get_supported_langs 30 | from roktracker.utils.validator import sanitize_scanname, validate_installation 31 | 32 | 33 | logger = logging.getLogger(__name__) 34 | ex_handler = ConsoleExceptionHander(logger) 35 | 36 | 37 | sys.excepthook = ex_handler.handle_exception 38 | threading.excepthook = ex_handler.handle_thread_exception 39 | 40 | 41 | def handle_exception(exc_type, exc_value, exc_traceback): 42 | if issubclass(exc_type, KeyboardInterrupt): 43 | sys.__excepthook__(exc_type, exc_value, exc_traceback) 44 | return 45 | 46 | logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) 47 | 48 | 49 | sys.excepthook = handle_exception 50 | 51 | 52 | def ask_abort(alliance_scanner: AllianceScanner) -> None: 53 | stop = questionary.confirm( 54 | message="Do you want to stop the scanner?:", auto_enter=False, default=False 55 | ).ask() 56 | 57 | if stop: 58 | console.print("Scan will aborted after next governor.") 59 | alliance_scanner.end_scan() 60 | 61 | 62 | def main(): 63 | if not validate_installation().success: 64 | sys.exit(2) 65 | 66 | root_dir = get_app_root() 67 | 68 | try: 69 | config = load_config() 70 | except ConfigError as e: 71 | logger.fatal(str(e)) 72 | console.log(str(e)) 73 | sys.exit(3) 74 | 75 | console.print( 76 | "Tesseract languages available: " 77 | + get_supported_langs(str(root_dir / "deps" / "tessdata")) 78 | ) 79 | 80 | try: 81 | bluestacks_device_name = questionary.text( 82 | message="Name of your bluestacks instance:", 83 | default=config["general"]["bluestacks"]["name"], 84 | ).unsafe_ask() 85 | 86 | bluestacks_port = int( 87 | questionary.text( 88 | f"Adb port of device (detected {get_bluestacks_port(bluestacks_device_name, config)}):", 89 | default=str(get_bluestacks_port(bluestacks_device_name, config)), 90 | validate=lambda port: is_string_int(port), 91 | ).unsafe_ask() 92 | ) 93 | 94 | kingdom = questionary.text( 95 | message="Alliance name (used for file name):", 96 | default=config["scan"]["kingdom_name"], 97 | ).unsafe_ask() 98 | 99 | validated_name = sanitize_scanname(kingdom) 100 | while not validated_name.valid: 101 | kingdom = questionary.text( 102 | message="Alliance name (Previous name was invalid):", 103 | default=validated_name.result, 104 | ).unsafe_ask() 105 | validated_name = sanitize_scanname(kingdom) 106 | 107 | scan_amount = int( 108 | questionary.text( 109 | message="Number of people to scan:", 110 | validate=lambda port: is_string_int(port), 111 | default=str(config["scan"]["people_to_scan"]), 112 | ).unsafe_ask() 113 | ) 114 | 115 | save_formats = OutputFormats() 116 | save_formats_tmp = questionary.checkbox( 117 | "In what format should the result be saved?", 118 | choices=[ 119 | questionary.Choice( 120 | "Excel (xlsx)", 121 | value="xlsx", 122 | checked=config["scan"]["formats"]["xlsx"], 123 | ), 124 | questionary.Choice( 125 | "Comma seperated values (csv)", 126 | value="csv", 127 | checked=config["scan"]["formats"]["csv"], 128 | ), 129 | questionary.Choice( 130 | "JSON Lines (jsonl)", 131 | value="jsonl", 132 | checked=config["scan"]["formats"]["jsonl"], 133 | ), 134 | ], 135 | ).unsafe_ask() 136 | 137 | if save_formats_tmp == [] or save_formats_tmp == None: 138 | console.print("Exiting, no formats selected.") 139 | return 140 | else: 141 | save_formats.from_list(save_formats_tmp) 142 | except: 143 | console.log("User abort. Exiting scanner.") 144 | sys.exit(3) 145 | 146 | try: 147 | alliance_scanner = AllianceScanner(bluestacks_port, config) 148 | alliance_scanner.set_batch_callback(print_batch) 149 | 150 | console.print( 151 | f"The UUID of this scan is [green]{alliance_scanner.run_id}[/green]", 152 | highlight=False, 153 | ) 154 | signal.signal(signal.SIGINT, lambda _, __: ask_abort(alliance_scanner)) 155 | 156 | alliance_scanner.start_scan(kingdom, scan_amount, save_formats) 157 | except AdbError as error: 158 | logger.error( 159 | "An error with the adb connection occured (probably wrong port). Exact message: " 160 | + str(error) 161 | ) 162 | console.print( 163 | "An error with the adb connection occured. Please verfiy that you use the correct port.\nExact message: " 164 | + str(error) 165 | ) 166 | 167 | 168 | if __name__ == "__main__": 169 | main() 170 | input("Press enter to exit...") 171 | -------------------------------------------------------------------------------- /roktracker/honor/scanner.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | from cv2.typing import MatLike 5 | from dummy_root import get_app_root 6 | from roktracker.alliance.additional_data import AdditionalData 7 | from roktracker.alliance.governor_data import GovernorData 8 | from roktracker.alliance.governor_image_group import GovImageGroup 9 | from roktracker.alliance.pandas_handler import PandasHandler 10 | from roktracker.honor.ui_settings import HonorUI 11 | from roktracker.utils.adb import * 12 | from roktracker.utils.general import * 13 | from roktracker.utils.ocr import * 14 | from tesserocr import PyTessBaseAPI, PSM, OEM # type: ignore 15 | from typing import Callable, List 16 | 17 | from roktracker.utils.output_formats import OutputFormats 18 | 19 | 20 | def default_batch_callback(govs: List[GovernorData], extra: AdditionalData) -> None: 21 | pass 22 | 23 | 24 | def default_state_callback(msg: str) -> None: 25 | pass 26 | 27 | 28 | def default_output_handler(msg: str) -> None: 29 | console.log(msg) 30 | pass 31 | 32 | 33 | class HonorScanner: 34 | def __init__(self, port, config): 35 | self.run_id = generate_random_id(8) 36 | self.start_date = datetime.date.today() 37 | self.stop_scan = False 38 | self.scan_times = [] 39 | 40 | self.reached_bottom = False 41 | self.govs_per_screen = 5 42 | self.screens_needed = 0 43 | 44 | self.max_random_delay = config["scan"]["timings"]["max_random"] 45 | 46 | # TODO: Load paths from config 47 | self.root_dir = get_app_root() 48 | self.tesseract_path = Path(self.root_dir / "deps" / "tessdata") 49 | self.img_path = Path(self.root_dir / "temp_images") 50 | self.img_path.mkdir(parents=True, exist_ok=True) 51 | self.scan_path = Path(self.root_dir / "scans_honor") 52 | self.scan_path.mkdir(parents=True, exist_ok=True) 53 | 54 | self.batch_callback = default_batch_callback 55 | self.state_callback = default_state_callback 56 | self.output_handler = default_output_handler 57 | 58 | self.adb_client = AdvancedAdbClient( 59 | str(self.root_dir / "deps" / "platform-tools" / "adb.exe"), 60 | port, 61 | config["general"]["emulator"], 62 | self.root_dir / "deps" / "inputs", 63 | ) 64 | 65 | def set_batch_callback( 66 | self, cb: Callable[[List[GovernorData], AdditionalData], None] 67 | ) -> None: 68 | self.batch_callback = cb 69 | 70 | def set_state_callback(self, cb: Callable[[str], None]): 71 | self.state_callback = cb 72 | 73 | def set_output_handler(self, cb: Callable[[str], None]): 74 | self.output_handler = cb 75 | 76 | def get_remaining_time(self, remaining_govs: int) -> float: 77 | return (sum(self.scan_times, start=0) / len(self.scan_times)) * remaining_govs 78 | 79 | def process_honor_screen(self, image: MatLike, position: int) -> GovImageGroup: 80 | # fmt: off 81 | gov_name_im = cropToRegion(image, HonorUI.name[position]) 82 | gov_name_im_bw = preprocessImage( 83 | gov_name_im, 3, HonorUI.misc.threshold, 84 | 12, HonorUI.misc.invert, 85 | ) 86 | 87 | gov_name_im_bw_small = preprocessImage( 88 | gov_name_im, 1, HonorUI.misc.threshold, 89 | 4, HonorUI.misc.invert, 90 | ) 91 | 92 | gov_score_im = cropToRegion(image, HonorUI.score[position]) 93 | gov_score_im_bw = preprocessImage( 94 | gov_score_im, 3, HonorUI.misc.threshold, 95 | 12, HonorUI.misc.invert, 96 | ) 97 | # fmt: on 98 | 99 | return GovImageGroup(gov_name_im_bw, gov_name_im_bw_small, gov_score_im_bw) 100 | 101 | def scan_screen(self, screen_number: int) -> List[GovernorData]: 102 | # Take screenshot to process 103 | self.adb_client.secure_adb_screencap().save(self.img_path / "currentState.png") 104 | image = load_cv2_img(self.img_path / "currentState.png", cv2.IMREAD_UNCHANGED) 105 | 106 | # Actual scanning 107 | govs = [] 108 | with PyTessBaseAPI( 109 | path=str(self.tesseract_path), psm=PSM.SINGLE_LINE, oem=OEM.LSTM_ONLY 110 | ) as api: 111 | for gov_number in range(0, self.govs_per_screen): 112 | gov = self.process_honor_screen(image, gov_number) 113 | api.SetPageSegMode(PSM.SINGLE_LINE) 114 | gov_name = ocr_text(api, gov.name_img) 115 | 116 | api.SetPageSegMode(PSM.SINGLE_WORD) 117 | gov_score = ocr_number(api, gov.score_img) 118 | 119 | # fmt: off 120 | gov_img_path = str(self.img_path / f"gov_name_{(6 * screen_number) + gov_number}.png") 121 | # fmt: on 122 | write_cv2_img( 123 | gov.name_img_small, 124 | gov_img_path, 125 | "png", 126 | ) 127 | 128 | govs.append(GovernorData(gov_img_path, gov_name, gov_score)) 129 | 130 | return govs 131 | 132 | def start_scan(self, kingdom: str, amount: int, formats: OutputFormats): 133 | self.state_callback("Initializing") 134 | self.adb_client.start_adb() 135 | self.screens_needed = int(math.ceil(amount / self.govs_per_screen)) 136 | 137 | filename = f"Honor{amount}-{self.start_date}-{kingdom}-[{self.run_id}].xlsx" 138 | data_handler = PandasHandler(self.scan_path, filename, formats) 139 | 140 | self.state_callback("Scanning") 141 | 142 | for i in range(0, self.screens_needed): 143 | if self.stop_scan: 144 | self.output_handler("Scan Terminated! Saving the current progress...") 145 | break 146 | 147 | start_time = time.time() 148 | governors = self.scan_screen(i) 149 | end_time = time.time() 150 | 151 | self.scan_times.append(end_time - start_time) 152 | 153 | additional_data = AdditionalData( 154 | i, 155 | amount, 156 | self.govs_per_screen, 157 | self.get_remaining_time(self.screens_needed - i), 158 | ) 159 | 160 | self.batch_callback(governors, additional_data) 161 | 162 | self.reached_bottom = ( 163 | data_handler.write_governors(governors) or self.reached_bottom 164 | ) 165 | data_handler.save() 166 | 167 | if self.reached_bottom: 168 | break 169 | else: 170 | self.adb_client.adb_send_events("Touch", HonorUI.misc.script) 171 | wait_random_range(1, self.max_random_delay) 172 | 173 | data_handler.save() 174 | self.adb_client.kill_adb() # make sure to clean up adb server 175 | 176 | for p in self.img_path.glob("gov_name*.png"): 177 | p.unlink() 178 | 179 | self.state_callback("Scan finished") 180 | 181 | return 182 | 183 | def end_scan(self) -> None: 184 | self.stop_scan = True 185 | -------------------------------------------------------------------------------- /rok_tracker.spec: -------------------------------------------------------------------------------- 1 | import customtkinter as ctk 2 | 3 | ctk.__path__[0].replace("\\", "/") 4 | 5 | added_files = [(ctk.__path__[0].replace("\\", "/"), "customtkinter/")] 6 | 7 | alliance_console_a = Analysis( 8 | ["alliance_scanner_console.py"], 9 | pathex=[], 10 | binaries=[], 11 | datas=[], 12 | hiddenimports=[], 13 | hookspath=[], 14 | hooksconfig={}, 15 | runtime_hooks=[], 16 | excludes=[], 17 | noarchive=False, 18 | ) 19 | alliance_console_pyz = PYZ(alliance_console_a.pure) 20 | 21 | alliance_console_exe = EXE( 22 | alliance_console_pyz, 23 | alliance_console_a.scripts, 24 | [], 25 | exclude_binaries=True, 26 | name="Alliance Scanner (CLI)", 27 | debug=False, 28 | bootloader_ignore_signals=False, 29 | strip=False, 30 | upx=True, 31 | console=True, 32 | disable_windowed_traceback=False, 33 | argv_emulation=False, 34 | target_arch=None, 35 | codesign_identity=None, 36 | entitlements_file=None, 37 | ) 38 | 39 | # ----------------------------------------------------- 40 | 41 | alliance_ui_a = Analysis( 42 | ["alliance_scanner_ui.py"], 43 | pathex=[], 44 | binaries=[], 45 | datas=added_files, 46 | hiddenimports=[], 47 | hookspath=[], 48 | hooksconfig={}, 49 | runtime_hooks=[], 50 | excludes=[], 51 | noarchive=False, 52 | ) 53 | alliance_ui_pyz = PYZ(alliance_ui_a.pure) 54 | 55 | alliance_ui_exe = EXE( 56 | alliance_ui_pyz, 57 | alliance_ui_a.scripts, 58 | [], 59 | exclude_binaries=True, 60 | name="Alliance Scanner (GUI)", 61 | debug=False, 62 | bootloader_ignore_signals=False, 63 | strip=False, 64 | upx=True, 65 | console=False, 66 | disable_windowed_traceback=False, 67 | argv_emulation=False, 68 | target_arch=None, 69 | codesign_identity=None, 70 | entitlements_file=None, 71 | ) 72 | 73 | # ----------------------------------------------------- 74 | 75 | honor_console_a = Analysis( 76 | ["honor_scanner_console.py"], 77 | pathex=[], 78 | binaries=[], 79 | datas=[], 80 | hiddenimports=[], 81 | hookspath=[], 82 | hooksconfig={}, 83 | runtime_hooks=[], 84 | excludes=[], 85 | noarchive=False, 86 | ) 87 | honor_console_pyz = PYZ(honor_console_a.pure) 88 | 89 | honor_console_exe = EXE( 90 | honor_console_pyz, 91 | honor_console_a.scripts, 92 | [], 93 | exclude_binaries=True, 94 | name="Honor Scanner (CLI)", 95 | debug=False, 96 | bootloader_ignore_signals=False, 97 | strip=False, 98 | upx=True, 99 | console=True, 100 | disable_windowed_traceback=False, 101 | argv_emulation=False, 102 | target_arch=None, 103 | codesign_identity=None, 104 | entitlements_file=None, 105 | ) 106 | 107 | # ------------------------------------------------------ 108 | 109 | honor_ui_a = Analysis( 110 | ["honor_scanner_ui.py"], 111 | pathex=[], 112 | binaries=[], 113 | datas=added_files, 114 | hiddenimports=[], 115 | hookspath=[], 116 | hooksconfig={}, 117 | runtime_hooks=[], 118 | excludes=[], 119 | noarchive=False, 120 | ) 121 | honor_ui_pyz = PYZ(honor_ui_a.pure) 122 | 123 | honor_ui_exe = EXE( 124 | honor_ui_pyz, 125 | honor_ui_a.scripts, 126 | [], 127 | exclude_binaries=True, 128 | name="Honor Scanner (GUI)", 129 | debug=False, 130 | bootloader_ignore_signals=False, 131 | strip=False, 132 | upx=True, 133 | console=False, 134 | disable_windowed_traceback=False, 135 | argv_emulation=False, 136 | target_arch=None, 137 | codesign_identity=None, 138 | entitlements_file=None, 139 | ) 140 | 141 | # --------------------------------------------------- 142 | 143 | kingdom_console_a = Analysis( 144 | ["kingdom_scanner_console.py"], 145 | pathex=[], 146 | binaries=[], 147 | datas=[], 148 | hiddenimports=[], 149 | hookspath=[], 150 | hooksconfig={}, 151 | runtime_hooks=[], 152 | excludes=[], 153 | noarchive=False, 154 | ) 155 | kingdom_console_pyz = PYZ(kingdom_console_a.pure) 156 | 157 | kingdom_console_exe = EXE( 158 | kingdom_console_pyz, 159 | kingdom_console_a.scripts, 160 | [], 161 | exclude_binaries=True, 162 | name="Kingdom Scanner (CLI)", 163 | debug=False, 164 | bootloader_ignore_signals=False, 165 | strip=False, 166 | upx=True, 167 | console=True, 168 | disable_windowed_traceback=False, 169 | argv_emulation=False, 170 | target_arch=None, 171 | codesign_identity=None, 172 | entitlements_file=None, 173 | ) 174 | 175 | # -------------------------------------------------- 176 | 177 | kingdom_ui_a = Analysis( 178 | ["kingdom_scanner_ui.py"], 179 | pathex=[], 180 | binaries=[], 181 | datas=added_files, 182 | hiddenimports=[], 183 | hookspath=[], 184 | hooksconfig={}, 185 | runtime_hooks=[], 186 | excludes=[], 187 | noarchive=False, 188 | ) 189 | kingdom_ui_pyz = PYZ(kingdom_ui_a.pure) 190 | 191 | kingdom_ui_exe = EXE( 192 | kingdom_ui_pyz, 193 | kingdom_ui_a.scripts, 194 | [], 195 | exclude_binaries=True, 196 | name="Kingdom Scanner (GUI)", 197 | debug=False, 198 | bootloader_ignore_signals=False, 199 | strip=False, 200 | upx=True, 201 | console=False, 202 | disable_windowed_traceback=False, 203 | argv_emulation=False, 204 | target_arch=None, 205 | codesign_identity=None, 206 | entitlements_file=None, 207 | ) 208 | 209 | # ------------------------------------------------- 210 | 211 | seed_console_a = Analysis( 212 | ["seed_scanner_console.py"], 213 | pathex=[], 214 | binaries=[], 215 | datas=[], 216 | hiddenimports=[], 217 | hookspath=[], 218 | hooksconfig={}, 219 | runtime_hooks=[], 220 | excludes=[], 221 | noarchive=False, 222 | ) 223 | seed_console_pyz = PYZ(seed_console_a.pure) 224 | 225 | seed_console_exe = EXE( 226 | seed_console_pyz, 227 | seed_console_a.scripts, 228 | [], 229 | exclude_binaries=True, 230 | name="Seed Scanner (CLI)", 231 | debug=False, 232 | bootloader_ignore_signals=False, 233 | strip=False, 234 | upx=True, 235 | console=True, 236 | disable_windowed_traceback=False, 237 | argv_emulation=False, 238 | target_arch=None, 239 | codesign_identity=None, 240 | entitlements_file=None, 241 | ) 242 | 243 | # ----------------------------------------------------- 244 | 245 | seed_ui_a = Analysis( 246 | ["seed_scanner_ui.py"], 247 | pathex=[], 248 | binaries=[], 249 | datas=added_files, 250 | hiddenimports=[], 251 | hookspath=[], 252 | hooksconfig={}, 253 | runtime_hooks=[], 254 | excludes=[], 255 | noarchive=False, 256 | ) 257 | seed_ui_pyz = PYZ(seed_ui_a.pure) 258 | 259 | seed_ui_exe = EXE( 260 | seed_ui_pyz, 261 | seed_ui_a.scripts, 262 | [], 263 | exclude_binaries=True, 264 | name="Seed Scanner (GUI)", 265 | debug=False, 266 | bootloader_ignore_signals=False, 267 | strip=False, 268 | upx=True, 269 | console=False, 270 | disable_windowed_traceback=False, 271 | argv_emulation=False, 272 | target_arch=None, 273 | codesign_identity=None, 274 | entitlements_file=None, 275 | ) 276 | 277 | # ----------------------------------------------------- 278 | 279 | coll = COLLECT( 280 | alliance_console_exe, 281 | alliance_console_a.binaries, 282 | alliance_console_a.datas, 283 | alliance_ui_exe, 284 | alliance_ui_a.binaries, 285 | alliance_ui_a.datas, 286 | honor_console_exe, 287 | honor_console_a.binaries, 288 | honor_console_a.datas, 289 | honor_ui_exe, 290 | honor_ui_a.binaries, 291 | honor_ui_a.datas, 292 | kingdom_console_exe, 293 | kingdom_console_a.binaries, 294 | kingdom_console_a.datas, 295 | kingdom_ui_exe, 296 | kingdom_ui_a.binaries, 297 | kingdom_ui_a.datas, 298 | seed_console_exe, 299 | seed_console_a.binaries, 300 | seed_console_a.datas, 301 | seed_ui_exe, 302 | seed_ui_a.binaries, 303 | seed_ui_a.datas, 304 | strip=False, 305 | upx=True, 306 | upx_exclude=[], 307 | name="RoK Tracker", 308 | ) 309 | -------------------------------------------------------------------------------- /roktracker/seed/scanner.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | from cv2.typing import MatLike 5 | from dummy_root import get_app_root 6 | from roktracker.alliance.additional_data import AdditionalData 7 | from roktracker.alliance.governor_data import GovernorData 8 | from roktracker.alliance.governor_image_group import GovImageGroup 9 | from roktracker.alliance.pandas_handler import PandasHandler 10 | from roktracker.seed.ui_settings import KingdomUI 11 | from roktracker.utils.adb import * 12 | from roktracker.utils.general import * 13 | from roktracker.utils.ocr import * 14 | from roktracker.utils.output_formats import OutputFormats 15 | from tesserocr import PyTessBaseAPI, PSM, OEM # type: ignore 16 | from typing import Callable, List 17 | 18 | 19 | def default_batch_callback(govs: List[GovernorData], extra: AdditionalData) -> None: 20 | pass 21 | 22 | 23 | def default_state_callback(msg: str) -> None: 24 | pass 25 | 26 | 27 | def default_output_handler(msg: str) -> None: 28 | console.log(msg) 29 | pass 30 | 31 | 32 | class SeedScanner: 33 | def __init__(self, port, config): 34 | self.run_id = generate_random_id(8) 35 | self.start_date = datetime.date.today() 36 | self.stop_scan = False 37 | self.scan_times = [] 38 | 39 | self.reached_bottom = False 40 | self.govs_per_screen = 6 41 | self.screens_needed = 0 42 | 43 | self.max_random_delay = config["scan"]["timings"]["max_random"] 44 | 45 | # TODO: Load paths from config 46 | self.root_dir = get_app_root() 47 | self.tesseract_path = Path(self.root_dir / "deps" / "tessdata") 48 | self.img_path = Path(self.root_dir / "temp_images") 49 | self.img_path.mkdir(parents=True, exist_ok=True) 50 | self.scan_path = Path(self.root_dir / "scans_seed") 51 | self.scan_path.mkdir(parents=True, exist_ok=True) 52 | 53 | self.batch_callback = default_batch_callback 54 | self.state_callback = default_state_callback 55 | self.output_handler = default_output_handler 56 | 57 | self.adb_client = AdvancedAdbClient( 58 | str(self.root_dir / "deps" / "platform-tools" / "adb.exe"), 59 | port, 60 | config["general"]["emulator"], 61 | self.root_dir / "deps" / "inputs", 62 | ) 63 | 64 | def set_batch_callback( 65 | self, cb: Callable[[List[GovernorData], AdditionalData], None] 66 | ) -> None: 67 | self.batch_callback = cb 68 | 69 | def set_state_callback(self, cb: Callable[[str], None]): 70 | self.state_callback = cb 71 | 72 | def set_output_handler(self, cb: Callable[[str], None]): 73 | self.output_handler = cb 74 | 75 | def get_remaining_time(self, remaining_govs: int) -> float: 76 | return (sum(self.scan_times, start=0) / len(self.scan_times)) * remaining_govs 77 | 78 | def process_ranking_screen(self, image: MatLike, position: int) -> GovImageGroup: 79 | if not self.reached_bottom: 80 | # fmt: off 81 | gov_name_im = cropToRegion(image, KingdomUI.name_normal[position]) 82 | gov_name_im_bw = preprocessImage( 83 | gov_name_im, 3, KingdomUI.misc.threshold, 84 | 12, KingdomUI.misc.invert, 85 | ) 86 | 87 | gov_name_im_bw_small = preprocessImage( 88 | gov_name_im, 1, KingdomUI.misc.threshold, 89 | 4, KingdomUI.misc.invert, 90 | ) 91 | 92 | gov_score_im = cropToRegion(image, KingdomUI.score_normal[position]) 93 | gov_score_im_bw = preprocessImage( 94 | gov_score_im,3,KingdomUI.misc.threshold, 95 | 12,KingdomUI.misc.invert, 96 | ) 97 | # fmt: on 98 | else: 99 | # fmt: off 100 | gov_name_im = cropToRegion(image, KingdomUI.name_last[position]) 101 | gov_name_im_bw = preprocessImage( 102 | gov_name_im,3,KingdomUI.misc.threshold, 103 | 12,KingdomUI.misc.invert, 104 | ) 105 | 106 | gov_name_im_bw_small = preprocessImage( 107 | gov_name_im, 1, KingdomUI.misc.threshold, 108 | 4, KingdomUI.misc.invert, 109 | ) 110 | 111 | gov_score_im = cropToRegion(image, KingdomUI.score_last[position]) 112 | gov_score_im_bw = preprocessImage( 113 | gov_score_im,3,KingdomUI.misc.threshold, 114 | 12,KingdomUI.misc.invert, 115 | ) 116 | # fmt: on 117 | 118 | return GovImageGroup(gov_name_im_bw, gov_name_im_bw_small, gov_score_im_bw) 119 | 120 | def scan_screen(self, screen_number: int) -> List[GovernorData]: 121 | # Take screenshot to process 122 | self.adb_client.secure_adb_screencap().save(self.img_path / "currentState.png") 123 | image = load_cv2_img(self.img_path / "currentState.png", cv2.IMREAD_UNCHANGED) 124 | 125 | # Check for last screen in alliance mode 126 | with PyTessBaseAPI( 127 | path=str(self.tesseract_path), psm=PSM.SINGLE_WORD, oem=OEM.LSTM_ONLY 128 | ) as api: 129 | # fmt: off 130 | test_score_im = cropToRegion(image, KingdomUI.score_normal[0]) 131 | test_score_im_bw = preprocessImage( 132 | test_score_im,3,KingdomUI.misc.threshold, 133 | 12,KingdomUI.misc.invert, 134 | ) 135 | # fmt: on 136 | 137 | api.SetImage(Image.fromarray(test_score_im_bw)) # type: ignore 138 | test_score = api.GetUTF8Text() 139 | test_score = re.sub("[^0-9]", "", test_score) 140 | 141 | if test_score == "": 142 | self.reached_bottom = True 143 | 144 | # Actual scanning 145 | govs = [] 146 | with PyTessBaseAPI( 147 | path=str(self.tesseract_path), psm=PSM.SINGLE_LINE, oem=OEM.LSTM_ONLY 148 | ) as api: 149 | for gov_number in range(0, self.govs_per_screen): 150 | gov = self.process_ranking_screen(image, gov_number) 151 | api.SetPageSegMode(PSM.SINGLE_LINE) 152 | gov_name = ocr_text(api, gov.name_img) 153 | 154 | api.SetPageSegMode(PSM.SINGLE_WORD) 155 | gov_score = ocr_number(api, gov.score_img) 156 | 157 | # fmt: off 158 | gov_img_path = str(self.img_path / f"gov_name_{(6 * screen_number) + gov_number}.png") 159 | # fmt: on 160 | write_cv2_img( 161 | gov.name_img_small, 162 | gov_img_path, 163 | "png", 164 | ) 165 | 166 | govs.append(GovernorData(gov_img_path, gov_name, gov_score)) 167 | 168 | return govs 169 | 170 | def start_scan(self, kingdom: str, amount: int, formats: OutputFormats): 171 | self.state_callback("Initializing") 172 | self.adb_client.start_adb() 173 | self.screens_needed = int(math.ceil(amount / self.govs_per_screen)) 174 | 175 | filename = f"Seed{amount}-{self.start_date}-{kingdom}-[{self.run_id}]" 176 | data_handler = PandasHandler(self.scan_path, filename, formats) 177 | 178 | self.state_callback("Scanning") 179 | 180 | for i in range(0, self.screens_needed): 181 | if self.stop_scan: 182 | self.output_handler("Scan Terminated! Saving the current progress...") 183 | break 184 | 185 | start_time = time.time() 186 | governors = self.scan_screen(i) 187 | end_time = time.time() 188 | 189 | self.scan_times.append(end_time - start_time) 190 | 191 | additional_data = AdditionalData( 192 | i, 193 | amount, 194 | self.govs_per_screen, 195 | self.get_remaining_time(self.screens_needed - i), 196 | ) 197 | 198 | self.batch_callback(governors, additional_data) 199 | 200 | self.reached_bottom = ( 201 | data_handler.write_governors(governors) or self.reached_bottom 202 | ) 203 | data_handler.save() 204 | 205 | if self.reached_bottom: 206 | break 207 | else: 208 | self.adb_client.adb_send_events("Touch", KingdomUI.misc.script) 209 | wait_random_range(1, self.max_random_delay) 210 | 211 | data_handler.save(amount, True) 212 | self.adb_client.kill_adb() # make sure to clean up adb server 213 | 214 | for p in self.img_path.glob("gov_name*.png"): 215 | p.unlink() 216 | 217 | self.state_callback("Scan finished") 218 | 219 | return 220 | 221 | def end_scan(self) -> None: 222 | self.stop_scan = True 223 | -------------------------------------------------------------------------------- /roktracker/alliance/scanner.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | from cv2.typing import MatLike 5 | from dummy_root import get_app_root 6 | from roktracker.alliance.additional_data import AdditionalData 7 | from roktracker.alliance.governor_data import GovernorData 8 | from roktracker.alliance.governor_image_group import GovImageGroup 9 | from roktracker.alliance.pandas_handler import PandasHandler 10 | from roktracker.alliance.ui_settings import AllianceUI 11 | from roktracker.utils.adb import * 12 | from roktracker.utils.general import * 13 | from roktracker.utils.ocr import * 14 | from roktracker.utils.output_formats import OutputFormats 15 | from tesserocr import PyTessBaseAPI, PSM, OEM # type: ignore 16 | from typing import Callable, List 17 | 18 | 19 | def default_batch_callback(govs: List[GovernorData], extra: AdditionalData) -> None: 20 | pass 21 | 22 | 23 | def default_state_callback(msg: str) -> None: 24 | pass 25 | 26 | 27 | def default_output_handler(msg: str) -> None: 28 | console.log(msg) 29 | pass 30 | 31 | 32 | class AllianceScanner: 33 | def __init__(self, port, config): 34 | self.run_id = generate_random_id(8) 35 | self.start_date = datetime.date.today() 36 | self.stop_scan = False 37 | self.scan_times = [] 38 | 39 | self.reached_bottom = False 40 | self.govs_per_screen = 6 41 | self.screens_needed = 0 42 | 43 | self.max_random_delay = config["scan"]["timings"]["max_random"] 44 | 45 | # TODO: Load paths from config 46 | self.root_dir = get_app_root() 47 | self.tesseract_path = Path(self.root_dir / "deps" / "tessdata") 48 | self.img_path = Path(self.root_dir / "temp_images") 49 | self.img_path.mkdir(parents=True, exist_ok=True) 50 | self.scan_path = Path(self.root_dir / "scans_alliance") 51 | self.scan_path.mkdir(parents=True, exist_ok=True) 52 | 53 | self.batch_callback = default_batch_callback 54 | self.state_callback = default_state_callback 55 | self.output_handler = default_output_handler 56 | 57 | self.adb_client = AdvancedAdbClient( 58 | str(self.root_dir / "deps" / "platform-tools" / "adb.exe"), 59 | port, 60 | config["general"]["emulator"], 61 | self.root_dir / "deps" / "inputs", 62 | ) 63 | 64 | def set_batch_callback( 65 | self, cb: Callable[[List[GovernorData], AdditionalData], None] 66 | ) -> None: 67 | self.batch_callback = cb 68 | 69 | def set_state_callback(self, cb: Callable[[str], None]): 70 | self.state_callback = cb 71 | 72 | def set_output_handler(self, cb: Callable[[str], None]): 73 | self.output_handler = cb 74 | 75 | def get_remaining_time(self, remaining_govs: int) -> float: 76 | return (sum(self.scan_times, start=0) / len(self.scan_times)) * remaining_govs 77 | 78 | def process_alliance_screen(self, image: MatLike, position: int) -> GovImageGroup: 79 | if not self.reached_bottom: 80 | # fmt: off 81 | gov_name_im = cropToRegion(image, AllianceUI.name_normal[position]) 82 | gov_name_im_bw = preprocessImage( 83 | gov_name_im, 3, AllianceUI.misc.threshold, 84 | 12, AllianceUI.misc.invert, 85 | ) 86 | 87 | gov_name_im_bw_small = preprocessImage( 88 | gov_name_im, 1, AllianceUI.misc.threshold, 89 | 4, AllianceUI.misc.invert, 90 | ) 91 | 92 | gov_score_im = cropToRegion(image, AllianceUI.score_normal[position]) 93 | gov_score_im_bw = preprocessImage( 94 | gov_score_im,3,AllianceUI.misc.threshold, 95 | 12,AllianceUI.misc.invert, 96 | ) 97 | # fmt: on 98 | else: 99 | # fmt: off 100 | gov_name_im = cropToRegion(image, AllianceUI.name_last[position]) 101 | gov_name_im_bw = preprocessImage( 102 | gov_name_im,3,AllianceUI.misc.threshold, 103 | 12,AllianceUI.misc.invert, 104 | ) 105 | 106 | gov_name_im_bw_small = preprocessImage( 107 | gov_name_im, 1, AllianceUI.misc.threshold, 108 | 4, AllianceUI.misc.invert, 109 | ) 110 | 111 | gov_score_im = cropToRegion(image, AllianceUI.score_last[position]) 112 | gov_score_im_bw = preprocessImage( 113 | gov_score_im,3,AllianceUI.misc.threshold, 114 | 12,AllianceUI.misc.invert, 115 | ) 116 | # fmt: on 117 | 118 | return GovImageGroup(gov_name_im_bw, gov_name_im_bw_small, gov_score_im_bw) 119 | 120 | def scan_screen(self, screen_number: int) -> List[GovernorData]: 121 | # Take screenshot to process 122 | self.adb_client.secure_adb_screencap().save(self.img_path / "currentState.png") 123 | image = load_cv2_img(self.img_path / "currentState.png", cv2.IMREAD_UNCHANGED) 124 | 125 | # Check for last screen in alliance mode 126 | with PyTessBaseAPI( 127 | path=str(self.tesseract_path), psm=PSM.SINGLE_WORD, oem=OEM.LSTM_ONLY 128 | ) as api: 129 | # fmt: off 130 | test_score_im = cropToRegion(image, AllianceUI.score_normal[0]) 131 | test_score_im_bw = preprocessImage( 132 | test_score_im,3,AllianceUI.misc.threshold, 133 | 12,AllianceUI.misc.invert, 134 | ) 135 | # fmt: on 136 | 137 | api.SetImage(Image.fromarray(test_score_im_bw)) # type: ignore 138 | test_score = api.GetUTF8Text() 139 | test_score = re.sub("[^0-9]", "", test_score) 140 | 141 | if test_score == "": 142 | self.reached_bottom = True 143 | 144 | # Actual scanning 145 | govs = [] 146 | with PyTessBaseAPI( 147 | path=str(self.tesseract_path), psm=PSM.SINGLE_LINE, oem=OEM.LSTM_ONLY 148 | ) as api: 149 | for gov_number in range(0, self.govs_per_screen): 150 | gov = self.process_alliance_screen(image, gov_number) 151 | api.SetPageSegMode(PSM.SINGLE_LINE) 152 | gov_name = ocr_text(api, gov.name_img) 153 | 154 | api.SetPageSegMode(PSM.SINGLE_WORD) 155 | gov_score = ocr_number(api, gov.score_img) 156 | 157 | # fmt: off 158 | gov_img_path = str(self.img_path / f"gov_name_{(6 * screen_number) + gov_number}.png") 159 | # fmt: on 160 | write_cv2_img( 161 | gov.name_img_small, 162 | gov_img_path, 163 | "png", 164 | ) 165 | 166 | govs.append(GovernorData(gov_img_path, gov_name, gov_score)) 167 | 168 | return govs 169 | 170 | def start_scan(self, kingdom: str, amount: int, formats: OutputFormats): 171 | self.state_callback("Initializing") 172 | self.adb_client.start_adb() 173 | self.screens_needed = int(math.ceil(amount / self.govs_per_screen)) 174 | 175 | filename = f"Alliance{amount}-{self.start_date}-{kingdom}-[{self.run_id}]" 176 | data_handler = PandasHandler(self.scan_path, filename, formats) 177 | 178 | self.state_callback("Scanning") 179 | 180 | for i in range(0, self.screens_needed): 181 | if self.stop_scan: 182 | self.output_handler("Scan Terminated! Saving the current progress...") 183 | break 184 | 185 | start_time = time.time() 186 | governors = self.scan_screen(i) 187 | end_time = time.time() 188 | 189 | self.scan_times.append(end_time - start_time) 190 | 191 | additional_data = AdditionalData( 192 | i, 193 | amount, 194 | self.govs_per_screen, 195 | self.get_remaining_time(self.screens_needed - i), 196 | ) 197 | 198 | self.batch_callback(governors, additional_data) 199 | 200 | self.reached_bottom = ( 201 | data_handler.write_governors(governors) or self.reached_bottom 202 | ) 203 | data_handler.save() 204 | 205 | if self.reached_bottom: 206 | break 207 | else: 208 | self.adb_client.adb_send_events("Touch", AllianceUI.misc.script) 209 | wait_random_range(1, self.max_random_delay) 210 | 211 | data_handler.save() 212 | self.adb_client.kill_adb() # make sure to clean up adb server 213 | 214 | for p in self.img_path.glob("gov_name*.png"): 215 | p.unlink() 216 | 217 | self.state_callback("Scan finished") 218 | 219 | return 220 | 221 | def end_scan(self) -> None: 222 | self.stop_scan = True 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rok Tracker 2 | 3 | ## Important Notes 4 | * Version 5 config is not compatible with Version 4 config, 5 | so you need to adjust the config accordingly. 6 | * It is important to use `\\` in the config json bluestacks path (not `\`), otherwise you will get a JSONDecodeError. Please open no issues for that. 7 | 8 | ## Latest Changes 9 | * Experimental support for LD Player 10 | * [Wiki tab](https://github.com/Cyrexxis/RokTracker/wiki/) where more stuff gets explained 11 | * Seed Scanner that works like alliance scanner only for kingdom (fast way to get only kp or power) 12 | * Now possible to choose the output format (xlsx, jsonl or csv) 13 | * Default adb port can be changed 14 | * Option to check for plausible governor power during kingdom scan on the power ranking page 15 | 16 | ## Summary 17 | 18 | Open Source Rise of Kingdoms Stats Management Tool. Track TOP X Players in kingdom / alliance / honor leaderboard. Depending on what you scan the resulting .xlsx will look different: 19 | 20 | For kingdom rankings Governor Id, Governor Name, Power, Kill Points, Ranged Points, T1/T2/T3/T4/T5 Kills, Total kills, T4+T5 kills, Dead troops, RSS Gathered, RSS Assistance, Helps and Alliance name will get saved. 21 | 22 | For the honor and alliance rankings only the governor name and the score will be saved. Because there is no guarantee that the name is correct a picture of the name will be saved in addition. 23 | 24 | This is a heavily modified version of the original tool from [nikolakis1919](https://github.com/nikolakis1919) in the repository [https://github.com/nikolakis1919/RokTracker](https://github.com/nikolakis1919/RokTracker) 25 | 26 | There are two ways of installing and using the scanner. The first is an exe file with no need for other software to be installed. This approach is described under [simple installation](#simple-installation). 27 | The second way of using the scanner is by using the python files directly. However, for that approach you need to have a running python installation and have to install the dependencies manually. This is explained under [advanced installation](#advanced-installation). 28 | 29 | # Simple installation 30 | 31 | ## Required 32 | 33 | 1. Bluestacks 5 Installation [https://www.bluestacks.com/de/bluestacks-5.html](https://www.bluestacks.com/de/bluestacks-5.html) 34 | 2. To use tesserocr you also need to download the trained tesseract models [https://github.com/tesseract-ocr/tessdata](https://github.com/tesseract-ocr/tessdata) 35 | 3. Adb Platform Tools Download and Extract(See Important Notes) [https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip](https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip) 36 | 4. Tested on Windows 10 and 11, could work on Windows 7 37 | 38 | ## Setup 39 | 40 | 1. Download the latest release for your system here: [Latest Release](https://github.com/Cyrexxis/RokTracker/releases/latest) (choose RoK Tracker.zip) 41 | 2. Extract the zip in the folder where you want the scanner to be installed in 42 | 3. Download the requirements 2 (tessdata) and 3 (platform-tools) and extract them in the deps folder 43 | - tesseract needs to go into `deps/tessdata/` 44 | - platform-tools need to go into `deps/platform-tools` 45 | - complete tree how the folder should look like are [here](#simple-filefolder-structure) 46 | 4. Configure your Bluestacks instance according to the [instructions](#bluestacks-5-settings) 47 | 48 | ## Usage 49 | 50 | 1. Adjust the default options in the `config.json` file to your liking 51 | 2. Double-click the exe like any normal program and enjoy the scanner 52 | 3. The results of the scans can be found in the `scans-*` folder, where * is the type of scan you are doing 53 | 54 | ## Simple File/Folder Structure 55 | 56 | ``` 57 | ./ 58 | ├─ _internal/ 59 | │ ├─ ... 60 | ├─ deps/ 61 | │ ├─ inputs/ 62 | │ ├─ tessdata/ 63 | │ │ ├─ *.traineddata 64 | │ │ ├─ ... 65 | │ ├─ platform-tools/ 66 | │ │ ├─ adb.exe 67 | │ │ ├─ ... 68 | ├─ config.json 69 | ├─ *.exe 70 | ``` 71 | 72 | # Advanced installation 73 | 74 | ## Required 75 | 76 | 1. Bluestacks 5 Installation [https://www.bluestacks.com/de/bluestacks-5.html](https://www.bluestacks.com/de/bluestacks-5.html) 77 | 2. Python 3.11 Installation [https://www.python.org/downloads/release/python-3110/](https://www.python.org/downloads/release/python-3110/) 78 | 3. To use tesserocr you also need to download the trained tesseract models [https://github.com/tesseract-ocr/tessdata](https://github.com/tesseract-ocr/tessdata) 79 | 4. Adb Platform Tools Download and Extract(See Important Notes) [https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip](https://dl.google.com/android/repository/platform-tools_r31.0.3-windows.zip) 80 | 5. On Windows "Microsoft Build Tools für C++" might be required for some python packages [https://visualstudio.microsoft.com/de/visual-cpp-build-tools/](https://visualstudio.microsoft.com/de/visual-cpp-build-tools/) 81 | 6. Tested on Windows 10 and 11, could work on Windows 7. Tested with Python 3.11.0 and 3.11.7. 82 | 83 | ## Setup 84 | 85 | 1. Download the latest release for your system here: [Latest Release](https://github.com/Cyrexxis/RokTracker/releases/latest) (choose the source code option) 86 | 2. Download and install Python and Build Tools for C++ 87 | 3. Download the requirements 3 (tessdata) and 4 (platform-tools) and extract them in the deps folder 88 | - tesseract needs to go into `deps/tessdata/` 89 | - platform-tools need to go into `deps/platform-tools` 90 | - complete tree how the folder should look like are [here](#simple-filefolder-structure) 91 | 4. Open your terminal in this folder and create a venv via `python -m venv venv` 92 | 5. Activate that venv via `./venv/Scripts/activate` 93 | 6. Install the python requirements via `pip install -r requirements_win64.txt` 94 | 7. Configure your Bluestacks instance according to the [instructions](#bluestacks-5-settings) 95 | 96 | ## Usage 97 | 98 | 1. Open a terminal in your rok tracker folder 99 | 2. Activate the venv via `./venv/Scripts/activate` 100 | 3. Start the scanner: 101 | - `python kingdom_scanner_console.py` for the CLI version of the kingdom scanner 102 | - `python kingdom_scanner_ui.py` for the GUI version of the kingdom scanner 103 | - `python alliance_scanner_console.py` for the CLI version of the alliance scanner 104 | - `python alliance_scanner_ui.py` for the GUI version of the alliance scanner 105 | - `python honor_scanner_console.py` for the CLI version of the honor scanner 106 | - `python honor_scanner_ui.py` for the GUI version of the honor scanner 107 | 4. The results of the scans can be found in the `scans-*` folder, where * is the type of scan you are doing 108 | 109 | ## Folder/File Structure 110 | 111 | ``` 112 | ./ 113 | ├─ deps/ 114 | │ ├─ inputs/ 115 | │ ├─ tessdata/ 116 | │ │ ├─ *.traineddata 117 | │ │ ├─ ... 118 | │ ├─ platform-tools/ 119 | │ │ ├─ adb.exe 120 | │ │ ├─ ... 121 | ├─ config.json 122 | ├─ *_scanner_console.py 123 | ├─ *_scanner_ui.py 124 | ├─ ... 125 | ``` 126 | 127 | # Features 128 | 129 | ## Kingdom Scanner 130 | 131 | - Complete kingdom ranking scan 132 | - Detection for wrong kills based on if kills to kill points are correct 133 | - In case something doesn't check out the corresponding images are saved in the `manual_review`-folder (prefix F) and a warning is logged in the log file. 134 | - Option to try to recover kills if wrong kills are detected 135 | - Nevertheless the same images as for a fail get copied to the `manual_review`-folder (prefix R) and an info is logged in the log file. 136 | - Detection of inactive accounts 137 | - an inactive account is an account that cannot be clicked in the kingdom rankings 138 | - those are skipped automatically, and it is optionally possible to save a screenshot of the name in the `inactives`-folder. 139 | 140 | ## Alliance and Honor Scanner 141 | 142 | - complete alliance ranking scan 143 | - complete personal honor ranking scan 144 | - due to how the game works the names are very inaccurate, and it is not possible to track the governor ID 145 | 146 | # Bluestacks 5 Settings 147 | 148 | ## Main configuration 149 | 150 | - Display Tab ([Screenshot](images/bluestacks-display.png)) 151 | - Resolution: 1600x900 152 | - DPI: Custom (450) 153 | - Advanced Tab ([Screenshot](images/bluestacks-advanced.png)) 154 | - Android Debug Bride: Turned on 155 | 156 | ## Configuration for automatic port detection 157 | - Change the entry `bluestacks_config` in the `config.json` file of the scanner to match the location of your conf file 158 | - This file is located in your Bluestacks installation folder. If you don't know where you installed it, chances are good that it is located at `C:\ProgramData\Bluestacks_nxt\bluestacks.conf` 159 | - Make sure to use the correct instance name. The scanner asks for a Bluestacks name and looks for that name in the config file 160 | - If you don't find a `bluestacks.conf` file chances are high that your installation always uses the same port. The default should be 5555, that's also what the scanner assumes. If you change the port you have to manually change it in the scanner. 161 | 162 | # Important Notes 163 | 164 | ## General 165 | 166 | 1. It is recommended to use a venv to keep your global python installation clean of the dependencies. After setting up the venv and activating it, you can install the requirements with the requirements.txt file (more about that in the "Usage" section) 167 | 2. The scanner does not need admin privilege to run 168 | 3. Make sure your folder structure matches the expected structure 169 | 4. Bluestacks settings must be the same as in pictures above. THIS IS VERY IMPORTANT! 170 | 5. BE CAREFUL to always copy the .xlsx file from the scans folder when it is finished, because in the next capture, there is a chance for it to get overwritten (very low chance). 171 | 6. Chinese letters might not be shown properly in CMD, but they are visible in the final .xlsx file. 172 | 7. You can do whatever you want in your computer when tool is scanning. Only be warned that copying can result in wrong governor names if it coincides with the name copy of the script. 173 | 8. Game Language has to be English. Anything else will cause trouble in detecting inactive governors. Change it only for scan, if yours is different and then switch back. 174 | 175 | ## Scan 176 | 177 | 1. In order to get only your kingdoms ranks, the character that is currently logged in game must be in HOME KINGDOM, else you will get all the players in your KvK including players from different kingdoms. 178 | 2. The view before running the program should be at the top of power rankings or at the top of kill points rankings for the `rok-scanner.py` or at the top of an alliance leader board of your choice or at top of the personal honor rank for the `alliance-scanner.py`. No move should be made in this window until scanning is done. 179 | 3. [Only for kingdom scan] Account must be lower in ranks than the amount of players you want to scan. e.g. Cannot scan top 100 when character's rank is 85. Use a farm account instead. 180 | 4. [Only for kingdom scan] Resume Scan option starts scanning the middle governor that is displayed in screen. The 4th in order. So before starting the tool make sure that you are in the correct view in bluestacks. 181 | 182 | # Error reporting / help 183 | Recently people started to random guess my discord name. So I'll just make it public here, it's simply cyrexxis same as my Github username. 184 | 185 | I try my best to help those people, but I will set up some rules for help requests: 186 | * Send me a message request (I am on the official RoK Server and the Chisgule server) 187 | * In this message request explain your problem and post your log file. 188 | * Simply saying it doesn't work is not enough, and you will get ignored 189 | * If you respect that you should get an answer from me. But there is no guarantee, because I am doing this project fully in my free time. 190 | 191 | A second way I will set up is Github discussions. The advantage with that is that other people see the mistakes and errors, maybe it helps them to fix them by their own. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,visualstudio,venv 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,visualstudio,venv 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv*/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | 166 | ### Python Patch ### 167 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 168 | poetry.toml 169 | 170 | # ruff 171 | .ruff_cache/ 172 | 173 | ### venv ### 174 | # Virtualenv 175 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 176 | [Bb]in 177 | [Ii]nclude 178 | [Ll]ib 179 | [Ll]ib64 180 | [Ll]ocal 181 | [Ss]cripts 182 | pyvenv.cfg 183 | pip-selfcheck.json 184 | 185 | ### VisualStudioCode ### 186 | .vscode/* 187 | !.vscode/settings.json 188 | !.vscode/tasks.json 189 | !.vscode/launch.json 190 | !.vscode/extensions.json 191 | !.vscode/*.code-snippets 192 | 193 | # Local History for Visual Studio Code 194 | .history/ 195 | 196 | # Built Visual Studio Code Extensions 197 | *.vsix 198 | 199 | ### VisualStudioCode Patch ### 200 | # Ignore all local history of files 201 | .history 202 | .ionide 203 | 204 | ### VisualStudio ### 205 | ## Ignore Visual Studio temporary files, build results, and 206 | ## files generated by popular Visual Studio add-ons. 207 | ## 208 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 209 | 210 | # User-specific files 211 | *.rsuser 212 | *.suo 213 | *.user 214 | *.userosscache 215 | *.sln.docstates 216 | 217 | # User-specific files (MonoDevelop/Xamarin Studio) 218 | *.userprefs 219 | 220 | # Mono auto generated files 221 | mono_crash.* 222 | 223 | # Build results 224 | [Dd]ebug/ 225 | [Dd]ebugPublic/ 226 | [Rr]elease/ 227 | [Rr]eleases/ 228 | x64/ 229 | x86/ 230 | [Ww][Ii][Nn]32/ 231 | [Aa][Rr][Mm]/ 232 | [Aa][Rr][Mm]64/ 233 | bld/ 234 | [Bb]in/ 235 | [Oo]bj/ 236 | [Ll]og/ 237 | [Ll]ogs/ 238 | 239 | # Visual Studio 2015/2017 cache/options directory 240 | .vs/ 241 | # Uncomment if you have tasks that create the project's static files in wwwroot 242 | #wwwroot/ 243 | 244 | # Visual Studio 2017 auto generated files 245 | Generated\ Files/ 246 | 247 | # MSTest test Results 248 | [Tt]est[Rr]esult*/ 249 | [Bb]uild[Ll]og.* 250 | 251 | # NUnit 252 | *.VisualState.xml 253 | TestResult.xml 254 | nunit-*.xml 255 | 256 | # Build Results of an ATL Project 257 | [Dd]ebugPS/ 258 | [Rr]eleasePS/ 259 | dlldata.c 260 | 261 | # Benchmark Results 262 | BenchmarkDotNet.Artifacts/ 263 | 264 | # .NET Core 265 | project.lock.json 266 | project.fragment.lock.json 267 | artifacts/ 268 | 269 | # ASP.NET Scaffolding 270 | ScaffoldingReadMe.txt 271 | 272 | # StyleCop 273 | StyleCopReport.xml 274 | 275 | # Files built by Visual Studio 276 | *_i.c 277 | *_p.c 278 | *_h.h 279 | *.ilk 280 | *.meta 281 | *.obj 282 | *.iobj 283 | *.pch 284 | *.pdb 285 | *.ipdb 286 | *.pgc 287 | *.pgd 288 | *.rsp 289 | *.sbr 290 | *.tlb 291 | *.tli 292 | *.tlh 293 | *.tmp 294 | *.tmp_proj 295 | *_wpftmp.csproj 296 | *.tlog 297 | *.vspscc 298 | *.vssscc 299 | .builds 300 | *.pidb 301 | *.svclog 302 | *.scc 303 | 304 | # Chutzpah Test files 305 | _Chutzpah* 306 | 307 | # Visual C++ cache files 308 | ipch/ 309 | *.aps 310 | *.ncb 311 | *.opendb 312 | *.opensdf 313 | *.sdf 314 | *.cachefile 315 | *.VC.db 316 | *.VC.VC.opendb 317 | 318 | # Visual Studio profiler 319 | *.psess 320 | *.vsp 321 | *.vspx 322 | *.sap 323 | 324 | # Visual Studio Trace Files 325 | *.e2e 326 | 327 | # TFS 2012 Local Workspace 328 | $tf/ 329 | 330 | # Guidance Automation Toolkit 331 | *.gpState 332 | 333 | # ReSharper is a .NET coding add-in 334 | _ReSharper*/ 335 | *.[Rr]e[Ss]harper 336 | *.DotSettings.user 337 | 338 | # TeamCity is a build add-in 339 | _TeamCity* 340 | 341 | # DotCover is a Code Coverage Tool 342 | *.dotCover 343 | 344 | # AxoCover is a Code Coverage Tool 345 | .axoCover/* 346 | !.axoCover/settings.json 347 | 348 | # Coverlet is a free, cross platform Code Coverage Tool 349 | coverage*.json 350 | coverage*.xml 351 | coverage*.info 352 | 353 | # Visual Studio code coverage results 354 | *.coverage 355 | *.coveragexml 356 | 357 | # NCrunch 358 | _NCrunch_* 359 | .*crunch*.local.xml 360 | nCrunchTemp_* 361 | 362 | # MightyMoose 363 | *.mm.* 364 | AutoTest.Net/ 365 | 366 | # Web workbench (sass) 367 | .sass-cache/ 368 | 369 | # Installshield output folder 370 | [Ee]xpress/ 371 | 372 | # DocProject is a documentation generator add-in 373 | DocProject/buildhelp/ 374 | DocProject/Help/*.HxT 375 | DocProject/Help/*.HxC 376 | DocProject/Help/*.hhc 377 | DocProject/Help/*.hhk 378 | DocProject/Help/*.hhp 379 | DocProject/Help/Html2 380 | DocProject/Help/html 381 | 382 | # Click-Once directory 383 | publish/ 384 | 385 | # Publish Web Output 386 | *.[Pp]ublish.xml 387 | *.azurePubxml 388 | # Note: Comment the next line if you want to checkin your web deploy settings, 389 | # but database connection strings (with potential passwords) will be unencrypted 390 | *.pubxml 391 | *.publishproj 392 | 393 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 394 | # checkin your Azure Web App publish settings, but sensitive information contained 395 | # in these scripts will be unencrypted 396 | PublishScripts/ 397 | 398 | # NuGet Packages 399 | *.nupkg 400 | # NuGet Symbol Packages 401 | *.snupkg 402 | # The packages folder can be ignored because of Package Restore 403 | **/[Pp]ackages/* 404 | # except build/, which is used as an MSBuild target. 405 | !**/[Pp]ackages/build/ 406 | # Uncomment if necessary however generally it will be regenerated when needed 407 | #!**/[Pp]ackages/repositories.config 408 | # NuGet v3's project.json files produces more ignorable files 409 | *.nuget.props 410 | *.nuget.targets 411 | 412 | # Microsoft Azure Build Output 413 | csx/ 414 | *.build.csdef 415 | 416 | # Microsoft Azure Emulator 417 | ecf/ 418 | rcf/ 419 | 420 | # Windows Store app package directories and files 421 | AppPackages/ 422 | BundleArtifacts/ 423 | Package.StoreAssociation.xml 424 | _pkginfo.txt 425 | *.appx 426 | *.appxbundle 427 | *.appxupload 428 | 429 | # Visual Studio cache files 430 | # files ending in .cache can be ignored 431 | *.[Cc]ache 432 | # but keep track of directories ending in .cache 433 | !?*.[Cc]ache/ 434 | 435 | # Others 436 | ClientBin/ 437 | ~$* 438 | *~ 439 | *.dbmdl 440 | *.dbproj.schemaview 441 | *.jfm 442 | *.pfx 443 | *.publishsettings 444 | orleans.codegen.cs 445 | 446 | # Including strong name files can present a security risk 447 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 448 | #*.snk 449 | 450 | # Since there are multiple workflows, uncomment next line to ignore bower_components 451 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 452 | #bower_components/ 453 | 454 | # RIA/Silverlight projects 455 | Generated_Code/ 456 | 457 | # Backup & report files from converting an old project file 458 | # to a newer Visual Studio version. Backup files are not needed, 459 | # because we have git ;-) 460 | _UpgradeReport_Files/ 461 | Backup*/ 462 | UpgradeLog*.XML 463 | UpgradeLog*.htm 464 | ServiceFabricBackup/ 465 | *.rptproj.bak 466 | 467 | # SQL Server files 468 | *.mdf 469 | *.ldf 470 | *.ndf 471 | 472 | # Business Intelligence projects 473 | *.rdl.data 474 | *.bim.layout 475 | *.bim_*.settings 476 | *.rptproj.rsuser 477 | *- [Bb]ackup.rdl 478 | *- [Bb]ackup ([0-9]).rdl 479 | *- [Bb]ackup ([0-9][0-9]).rdl 480 | 481 | # Microsoft Fakes 482 | FakesAssemblies/ 483 | 484 | # GhostDoc plugin setting file 485 | *.GhostDoc.xml 486 | 487 | # Node.js Tools for Visual Studio 488 | .ntvs_analysis.dat 489 | node_modules/ 490 | 491 | # Visual Studio 6 build log 492 | *.plg 493 | 494 | # Visual Studio 6 workspace options file 495 | *.opt 496 | 497 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 498 | *.vbw 499 | 500 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 501 | *.vbp 502 | 503 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 504 | *.dsw 505 | *.dsp 506 | 507 | # Visual Studio 6 technical files 508 | 509 | # Visual Studio LightSwitch build output 510 | **/*.HTMLClient/GeneratedArtifacts 511 | **/*.DesktopClient/GeneratedArtifacts 512 | **/*.DesktopClient/ModelManifest.xml 513 | **/*.Server/GeneratedArtifacts 514 | **/*.Server/ModelManifest.xml 515 | _Pvt_Extensions 516 | 517 | # Paket dependency manager 518 | .paket/paket.exe 519 | paket-files/ 520 | 521 | # FAKE - F# Make 522 | .fake/ 523 | 524 | # CodeRush personal settings 525 | .cr/personal 526 | 527 | # Python Tools for Visual Studio (PTVS) 528 | *.pyc 529 | 530 | # Cake - Uncomment if you are using it 531 | # tools/** 532 | # !tools/packages.config 533 | 534 | # Tabs Studio 535 | *.tss 536 | 537 | # Telerik's JustMock configuration file 538 | *.jmconfig 539 | 540 | # BizTalk build output 541 | *.btp.cs 542 | *.btm.cs 543 | *.odx.cs 544 | *.xsd.cs 545 | 546 | # OpenCover UI analysis results 547 | OpenCover/ 548 | 549 | # Azure Stream Analytics local run output 550 | ASALocalRun/ 551 | 552 | # MSBuild Binary and Structured Log 553 | *.binlog 554 | 555 | # NVidia Nsight GPU debugger configuration file 556 | *.nvuser 557 | 558 | # MFractors (Xamarin productivity tool) working folder 559 | .mfractor/ 560 | 561 | # Local History for Visual Studio 562 | .localhistory/ 563 | 564 | # Visual Studio History (VSHistory) files 565 | .vshistory/ 566 | 567 | # BeatPulse healthcheck temp database 568 | healthchecksdb 569 | 570 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 571 | MigrationBackup/ 572 | 573 | # Ionide (cross platform F# VS Code tools) working folder 574 | .ionide/ 575 | 576 | # Fody - auto-generated XML schema 577 | FodyWeavers.xsd 578 | 579 | # VS Code files for those working on multiple tools 580 | *.code-workspace 581 | 582 | # Local History for Visual Studio Code 583 | 584 | # Windows Installer files from build outputs 585 | *.cab 586 | *.msi 587 | *.msix 588 | *.msm 589 | *.msp 590 | 591 | # JetBrains Rider 592 | *.sln.iml 593 | 594 | ### VisualStudio Patch ### 595 | # Additional files built by Visual Studio 596 | 597 | # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,visualstudio,venv 598 | 599 | TOP* 600 | NEXT* 601 | inactive* 602 | alliance-dilated.jpeg 603 | alliance-eroted.jpeg 604 | alliance-eroded.jpeg 605 | alliance-ranking.png 606 | alliance-raw.jpeg 607 | check_more_info.png 608 | currentState.png 609 | kills_tier.afphoto 610 | kills_tier.png 611 | more_info.png 612 | platform-tools/ 613 | alliance.jpeg 614 | gov_info.png 615 | position_images/ 616 | manual_review/ 617 | temp_images/ 618 | scans/ 619 | scans_alliance/ 620 | scans_honor/ 621 | scans_kingdom/ 622 | scans_seed/ 623 | deps/tessdata 624 | deps/*.whl -------------------------------------------------------------------------------- /kingdom_scanner_console.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from dummy_root import get_app_root 4 | from roktracker.utils.check_python import check_py_version 5 | from roktracker.utils.exception_handling import ConsoleExceptionHander 6 | from roktracker.utils.output_formats import OutputFormats 7 | 8 | logging.basicConfig( 9 | filename=str(get_app_root() / "kingdom-scanner.log"), 10 | encoding="utf-8", 11 | format="%(asctime)s %(module)s %(levelname)s %(message)s", 12 | level=logging.INFO, 13 | datefmt="%Y-%m-%d %H:%M:%S", 14 | ) 15 | 16 | check_py_version((3, 11)) 17 | 18 | import json 19 | import questionary 20 | import signal 21 | import sys 22 | 23 | from roktracker.kingdom.governor_printer import print_gov_state 24 | from roktracker.kingdom.scanner import KingdomScanner 25 | from roktracker.utils.adb import * 26 | from roktracker.utils.console import console 27 | from roktracker.utils.general import * 28 | from roktracker.utils.ocr import get_supported_langs 29 | from roktracker.utils.validator import sanitize_scanname, validate_installation 30 | 31 | 32 | logger = logging.getLogger(__name__) 33 | ex_handler = ConsoleExceptionHander(logger) 34 | 35 | 36 | sys.excepthook = ex_handler.handle_exception 37 | threading.excepthook = ex_handler.handle_thread_exception 38 | 39 | 40 | def ask_abort(kingdom_scanner: KingdomScanner) -> None: 41 | stop = questionary.confirm( 42 | message="Do you want to stop the scanner?:", auto_enter=False, default=False 43 | ).ask() 44 | 45 | if stop: 46 | console.print("Scan will aborted after next governor.") 47 | kingdom_scanner.end_scan() 48 | 49 | 50 | def ask_continue(msg: str) -> bool: 51 | return questionary.confirm(message=msg, auto_enter=False, default=False).ask() 52 | 53 | 54 | def main(): 55 | if not validate_installation().success: 56 | sys.exit(2) 57 | root_dir = get_app_root() 58 | 59 | try: 60 | config = load_config() 61 | except ConfigError as e: 62 | logger.fatal(str(e)) 63 | console.log(str(e)) 64 | sys.exit(3) 65 | 66 | scan_options = { 67 | "ID": False, 68 | "Name": False, 69 | "Power": False, 70 | "Killpoints": False, 71 | "Alliance": False, 72 | "T1 Kills": False, 73 | "T2 Kills": False, 74 | "T3 Kills": False, 75 | "T4 Kills": False, 76 | "T5 Kills": False, 77 | "Ranged": False, 78 | "Deads": False, 79 | "Rss Assistance": False, 80 | "Rss Gathered": False, 81 | "Helps": False, 82 | } 83 | 84 | console.print( 85 | "Tesseract languages available: " 86 | + get_supported_langs(str(root_dir / "deps" / "tessdata")) 87 | ) 88 | 89 | try: 90 | bluestacks_device_name = questionary.text( 91 | message="Name of your bluestacks instance:", 92 | default=config["general"]["bluestacks"]["name"], 93 | ).unsafe_ask() 94 | 95 | bluestacks_port = int( 96 | questionary.text( 97 | f"Adb port of device (detected {get_bluestacks_port(bluestacks_device_name, config)}):", 98 | default=str(get_bluestacks_port(bluestacks_device_name, config)), 99 | validate=lambda port: is_string_int(port), 100 | ).unsafe_ask() 101 | ) 102 | 103 | kingdom = questionary.text( 104 | message="Kingdom name (used for file name):", 105 | default=config["scan"]["kingdom_name"], 106 | ).unsafe_ask() 107 | 108 | validated_name = sanitize_scanname(kingdom) 109 | while not validated_name.valid: 110 | kingdom = questionary.text( 111 | message="Kingdom name (Previous name was invalid):", 112 | default=validated_name.result, 113 | ).unsafe_ask() 114 | validated_name = sanitize_scanname(kingdom) 115 | 116 | scan_amount = int( 117 | questionary.text( 118 | message="Number of people to scan:", 119 | validate=lambda port: is_string_int(port), 120 | default=str(config["scan"]["people_to_scan"]), 121 | ).unsafe_ask() 122 | ) 123 | 124 | resume_scan = questionary.confirm( 125 | message="Resume scan:", 126 | auto_enter=False, 127 | default=config["scan"]["resume"], 128 | ).unsafe_ask() 129 | 130 | new_scroll = questionary.confirm( 131 | message="Use advanced scrolling method:", 132 | auto_enter=False, 133 | default=config["scan"]["advanced_scroll"], 134 | ).unsafe_ask() 135 | config["scan"]["advanced_scroll"] = new_scroll 136 | 137 | track_inactives = questionary.confirm( 138 | message="Screenshot inactives:", 139 | auto_enter=False, 140 | default=config["scan"]["track_inactives"], 141 | ).unsafe_ask() 142 | 143 | scan_mode = questionary.select( 144 | "What scan do you want to do?", 145 | choices=[ 146 | questionary.Choice( 147 | "Full (Everything the scanner can)", 148 | value="full", 149 | checked=True, 150 | shortcut_key="f", 151 | ), 152 | questionary.Choice( 153 | "Seed (ID, Name, Power, KP, Alliance)", 154 | value="seed", 155 | checked=False, 156 | shortcut_key="s", 157 | ), 158 | questionary.Choice( 159 | "Custom (select needed items in next step)", 160 | value="custom", 161 | checked=False, 162 | shortcut_key="c", 163 | ), 164 | ], 165 | ).unsafe_ask() 166 | 167 | match scan_mode: 168 | case "full": 169 | scan_options = { 170 | "ID": True, 171 | "Name": True, 172 | "Power": True, 173 | "Killpoints": True, 174 | "Alliance": True, 175 | "T1 Kills": True, 176 | "T2 Kills": True, 177 | "T3 Kills": True, 178 | "T4 Kills": True, 179 | "T5 Kills": True, 180 | "Ranged": True, 181 | "Deads": True, 182 | "Rss Assistance": True, 183 | "Rss Gathered": True, 184 | "Helps": True, 185 | } 186 | case "seed": 187 | scan_options = { 188 | "ID": True, 189 | "Name": True, 190 | "Power": True, 191 | "Killpoints": True, 192 | "Alliance": True, 193 | "T1 Kills": False, 194 | "T2 Kills": False, 195 | "T3 Kills": False, 196 | "T4 Kills": False, 197 | "T5 Kills": False, 198 | "Ranged": False, 199 | "Deads": False, 200 | "Rss Assistance": False, 201 | "Rss Gathered": False, 202 | "Helps": False, 203 | } 204 | case "custom": 205 | items_to_scan = questionary.checkbox( 206 | "What stats should be scanned?", 207 | choices=[ 208 | questionary.Choice( 209 | "ID", 210 | checked=False, 211 | ), 212 | questionary.Choice( 213 | "Name", 214 | checked=False, 215 | ), 216 | questionary.Choice( 217 | "Power", 218 | checked=False, 219 | ), 220 | questionary.Choice( 221 | "Killpoints", 222 | checked=False, 223 | ), 224 | questionary.Choice( 225 | "Alliance", 226 | checked=False, 227 | ), 228 | questionary.Choice( 229 | "T1 Kills", 230 | checked=False, 231 | ), 232 | questionary.Choice( 233 | "T2 Kills", 234 | checked=False, 235 | ), 236 | questionary.Choice( 237 | "T3 Kills", 238 | checked=False, 239 | ), 240 | questionary.Choice( 241 | "T4 Kills", 242 | checked=False, 243 | ), 244 | questionary.Choice( 245 | "T5 Kills", 246 | checked=False, 247 | ), 248 | questionary.Choice( 249 | "Ranged", 250 | checked=False, 251 | ), 252 | questionary.Choice( 253 | "Deads", 254 | checked=False, 255 | ), 256 | questionary.Choice( 257 | "Rss Assistance", 258 | checked=False, 259 | ), 260 | questionary.Choice( 261 | "Rss Gathered", 262 | checked=False, 263 | ), 264 | questionary.Choice( 265 | "Helps", 266 | checked=False, 267 | ), 268 | ], 269 | ).unsafe_ask() 270 | if items_to_scan == [] or items_to_scan == None: 271 | console.print("Exiting, no items selected.") 272 | return 273 | else: 274 | for item in items_to_scan: 275 | scan_options[item] = True 276 | case _: 277 | console.print("Exiting, no mode selected.") 278 | return 279 | 280 | validate_kills = False 281 | reconstruct_fails = False 282 | 283 | if ( 284 | scan_options["T1 Kills"] 285 | and scan_options["T2 Kills"] 286 | and scan_options["T3 Kills"] 287 | and scan_options["T4 Kills"] 288 | and scan_options["T5 Kills"] 289 | and scan_options["Killpoints"] 290 | ): 291 | validate_kills = questionary.confirm( 292 | message="Validate killpoints:", 293 | auto_enter=False, 294 | default=config["scan"]["validate_kills"], 295 | ).unsafe_ask() 296 | 297 | if validate_kills: 298 | reconstruct_fails = questionary.confirm( 299 | message="Try reconstructiong wrong kills values:", 300 | auto_enter=False, 301 | default=config["scan"]["reconstruct_kills"], 302 | ).unsafe_ask() 303 | 304 | validate_power = questionary.confirm( 305 | message="Validate power (only works in power ranking):", 306 | auto_enter=False, 307 | default=config["scan"]["validate_power"], 308 | ).unsafe_ask() 309 | 310 | power_threshold = int( 311 | questionary.text( 312 | message="Power threshold to trigger warning:", 313 | validate=lambda pt: is_string_int(pt), 314 | default=str(config["scan"]["power_threshold"]), 315 | ).unsafe_ask() 316 | ) 317 | 318 | config["scan"]["timings"]["info_close"] = float( 319 | questionary.text( 320 | message="Time to wait after more info close:", 321 | validate=lambda port: is_string_float(port), 322 | default=str(config["scan"]["timings"]["info_close"]), 323 | ).unsafe_ask() 324 | ) 325 | 326 | config["scan"]["timings"]["gov_close"] = float( 327 | questionary.text( 328 | message="Time to wait after governor close:", 329 | validate=lambda port: is_string_float(port), 330 | default=str(config["scan"]["timings"]["gov_close"]), 331 | ).unsafe_ask() 332 | ) 333 | 334 | save_formats = OutputFormats() 335 | save_formats_tmp = questionary.checkbox( 336 | "In what format should the result be saved?", 337 | choices=[ 338 | questionary.Choice( 339 | "Excel (xlsx)", 340 | value="xlsx", 341 | checked=config["scan"]["formats"]["xlsx"], 342 | ), 343 | questionary.Choice( 344 | "Comma seperated values (csv)", 345 | value="csv", 346 | checked=config["scan"]["formats"]["csv"], 347 | ), 348 | questionary.Choice( 349 | "JSON Lines (jsonl)", 350 | value="jsonl", 351 | checked=config["scan"]["formats"]["jsonl"], 352 | ), 353 | ], 354 | ).unsafe_ask() 355 | 356 | if save_formats_tmp == [] or save_formats_tmp == None: 357 | console.print("Exiting, no formats selected.") 358 | return 359 | else: 360 | save_formats.from_list(save_formats_tmp) 361 | except: 362 | console.log("User abort. Exiting scanner.") 363 | sys.exit(3) 364 | 365 | try: 366 | kingdom_scanner = KingdomScanner(config, scan_options, bluestacks_port) 367 | kingdom_scanner.set_continue_handler(ask_continue) 368 | kingdom_scanner.set_governor_callback(print_gov_state) 369 | 370 | console.print( 371 | f"The UUID of this scan is [green]{kingdom_scanner.run_id}[/green]", 372 | highlight=False, 373 | ) 374 | 375 | signal.signal(signal.SIGINT, lambda _, __: ask_abort(kingdom_scanner)) 376 | 377 | kingdom_scanner.start_scan( 378 | kingdom, 379 | scan_amount, 380 | resume_scan, 381 | track_inactives, 382 | validate_kills, 383 | reconstruct_fails, 384 | validate_power, 385 | power_threshold, 386 | save_formats, 387 | ) 388 | except AdbError as error: 389 | logger.error( 390 | "An error with the adb connection occured (probably wrong port). Exact message: " 391 | + str(error) 392 | ) 393 | console.print( 394 | "An error with the adb connection occured. Please verfiy that you use the correct port.\nExact message: " 395 | + str(error) 396 | ) 397 | 398 | 399 | if __name__ == "__main__": 400 | main() 401 | input("Press enter to exit...") 402 | -------------------------------------------------------------------------------- /honor_scanner_ui.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from dummy_root import get_app_root 4 | from roktracker.alliance.additional_data import AdditionalData 5 | from roktracker.alliance.governor_data import GovernorData 6 | from roktracker.honor.scanner import HonorScanner 7 | from roktracker.utils.check_python import check_py_version 8 | from roktracker.utils.exception_handling import GuiExceptionHandler 9 | from roktracker.utils.exceptions import AdbError, ConfigError 10 | from roktracker.utils.general import is_string_int, load_config 11 | from roktracker.utils.gui import InfoDialog 12 | from roktracker.utils.output_formats import OutputFormats 13 | 14 | logging.basicConfig( 15 | filename=str(get_app_root() / "honor-scanner.log"), 16 | encoding="utf-8", 17 | format="%(asctime)s %(module)s %(levelname)s %(message)s", 18 | level=logging.INFO, 19 | datefmt="%Y-%m-%d %H:%M:%S", 20 | ) 21 | 22 | check_py_version((3, 11)) 23 | 24 | import customtkinter 25 | import json 26 | import logging 27 | import os 28 | import sys 29 | 30 | from dummy_root import get_app_root 31 | from roktracker.utils.validator import sanitize_scanname, validate_installation 32 | from roktracker.utils.adb import get_bluestacks_port 33 | from threading import ExceptHookArgs, Thread 34 | from typing import Dict, List 35 | 36 | 37 | logger = logging.getLogger(__name__) 38 | ex_handler = GuiExceptionHandler(logger) 39 | 40 | sys.excepthook = ex_handler.handle_exception 41 | threading.excepthook = ex_handler.handle_thread_exception 42 | 43 | customtkinter.set_appearance_mode( 44 | "system" 45 | ) # Modes: "System" (standard), "Dark", "Light" 46 | customtkinter.set_default_color_theme( 47 | "blue" 48 | ) # Themes: "blue" (standard), "green", "dark-blue" 49 | 50 | 51 | def to_int_or(element, alternative): 52 | if element == "Skipped": 53 | return element 54 | 55 | try: 56 | return int(element) 57 | except ValueError: 58 | return alternative 59 | 60 | 61 | class CheckboxFrame(customtkinter.CTkTabview): 62 | def __init__(self, master, values, groupName): 63 | super().__init__( 64 | master, 65 | state="disabled", 66 | width=0, 67 | height=0, 68 | segmented_button_fg_color=customtkinter.ThemeManager.theme["CTkFrame"][ 69 | "fg_color" 70 | ], 71 | segmented_button_selected_color=customtkinter.ThemeManager.theme[ 72 | "CTkFrame" 73 | ]["fg_color"], 74 | text_color_disabled=customtkinter.ThemeManager.theme["CTkLabel"][ 75 | "text_color" 76 | ], 77 | ) 78 | self.add(groupName) 79 | self.values = list(filter(lambda x: x["group"] == groupName, values)) # type: ignore 80 | self.checkboxes: List[customtkinter.CTkCheckBox] = [] 81 | 82 | for i, value in enumerate(self.values): 83 | checkbox = customtkinter.CTkCheckBox( 84 | self.tab(groupName), 85 | text=value["name"], 86 | onvalue=True, 87 | offvalue=False, 88 | checkbox_height=16, 89 | checkbox_width=16, 90 | height=16, 91 | ) 92 | checkbox.grid(row=i, column=0, padx=10, pady=2, sticky="w") 93 | 94 | if value["default"]: 95 | checkbox.select() 96 | 97 | self.checkboxes.append(checkbox) 98 | 99 | def get(self): 100 | values = {} 101 | for checkbox in self.checkboxes: 102 | values.update({checkbox.cget("text"): checkbox.get()}) 103 | return values 104 | 105 | 106 | class HorizontalCheckboxFrame(customtkinter.CTkTabview): 107 | def __init__(self, master, values, groupName, options_per_row): 108 | super().__init__( 109 | master, 110 | state="disabled", 111 | width=0, 112 | height=0, 113 | segmented_button_fg_color=customtkinter.ThemeManager.theme["CTkFrame"][ 114 | "fg_color" 115 | ], 116 | segmented_button_selected_color=customtkinter.ThemeManager.theme[ 117 | "CTkFrame" 118 | ]["fg_color"], 119 | text_color_disabled=customtkinter.ThemeManager.theme["CTkLabel"][ 120 | "text_color" 121 | ], 122 | ) 123 | self.add(groupName) 124 | self.values = list(filter(lambda x: x["group"] == groupName, values)) # type: ignore 125 | self.checkboxes: List[Dict[str, customtkinter.CTkCheckBox]] = [] 126 | 127 | for i in range(0, options_per_row): 128 | self.tab(groupName).columnconfigure(i, weight=1) 129 | 130 | cur_row = 0 131 | for i, value in enumerate(self.values): 132 | cur_col = i % options_per_row 133 | label = customtkinter.CTkLabel( 134 | self.tab(groupName), text=value["name"], height=1 135 | ) 136 | label.grid(row=cur_row, column=cur_col, padx=10, pady=2) 137 | 138 | checkbox = customtkinter.CTkCheckBox( 139 | self.tab(groupName), 140 | text="", 141 | onvalue=True, 142 | offvalue=False, 143 | checkbox_height=20, 144 | checkbox_width=20, 145 | height=20, 146 | width=20, 147 | ) 148 | checkbox.grid(row=cur_row + 1, column=cur_col, padx=10, pady=2) 149 | 150 | if value["default"]: 151 | checkbox.select() 152 | 153 | self.checkboxes.append({value["name"]: checkbox}) 154 | 155 | if (i + 1) % options_per_row == 0: 156 | cur_row += 2 157 | 158 | def get(self): 159 | values = {} 160 | for checkbox in self.checkboxes: 161 | for k, v in checkbox.items(): 162 | values.update({k: bool(v.get())}) 163 | return values 164 | 165 | 166 | class BasicOptionsFame(customtkinter.CTkFrame): 167 | def __init__(self, master, config): 168 | super().__init__(master) 169 | self.config = config 170 | 171 | self.int_validation = self.register(is_string_int) 172 | 173 | self.grid_columnconfigure(0, weight=1) 174 | self.grid_columnconfigure(1, weight=2) 175 | self.scan_uuid_label = customtkinter.CTkLabel(self, text="Scan UUID:", height=1) 176 | self.scan_uuid_label.grid(row=0, column=0, padx=10, pady=(10, 0), sticky="w") 177 | self.scan_uuid_var = customtkinter.StringVar(self, "---") 178 | self.scan_uuid_label_2 = customtkinter.CTkLabel( 179 | self, textvariable=self.scan_uuid_var, height=1, anchor="w" 180 | ) 181 | self.scan_uuid_label_2.grid(row=0, column=1, padx=10, pady=(10, 0), sticky="ew") 182 | 183 | self.scan_name_label = customtkinter.CTkLabel(self, text="Scan name:", height=1) 184 | self.scan_name_label.grid(row=1, column=0, padx=10, pady=(10, 0), sticky="w") 185 | self.scan_name_text = customtkinter.CTkEntry(self) 186 | self.scan_name_text.grid(row=1, column=1, padx=10, pady=(10, 0), sticky="ew") 187 | self.scan_name_text.insert(0, config["scan"]["kingdom_name"]) 188 | 189 | self.bluestacks_instance_label = customtkinter.CTkLabel( 190 | self, text="Bluestacks name:", height=1 191 | ) 192 | self.bluestacks_instance_label.grid( 193 | row=2, column=0, padx=10, pady=(10, 0), sticky="w" 194 | ) 195 | self.bluestacks_instance_text = customtkinter.CTkEntry(self) 196 | self.bluestacks_instance_text.grid( 197 | row=2, column=1, padx=10, pady=(10, 0), sticky="ew" 198 | ) 199 | self.bluestacks_instance_text.insert(0, config["general"]["bluestacks"]["name"]) 200 | 201 | self.adb_port_label = customtkinter.CTkLabel(self, text="Adb port:", height=1) 202 | self.adb_port_label.grid(row=3, column=0, padx=10, pady=(10, 0), sticky="w") 203 | self.adb_port_text = customtkinter.CTkEntry( 204 | self, 205 | validate="all", 206 | validatecommand=(self.int_validation, "%P", True), 207 | ) 208 | self.adb_port_text.grid(row=3, column=1, padx=10, pady=(10, 0), sticky="ew") 209 | self.bluestacks_instance_text.configure( 210 | validatecommand=(self.register(self.update_port), "%P"), validate="key" 211 | ) 212 | self.update_port() 213 | 214 | self.scan_amount_label = customtkinter.CTkLabel( 215 | self, text="People to scan:", height=1 216 | ) 217 | self.scan_amount_label.grid(row=4, column=0, padx=10, pady=(10, 0), sticky="w") 218 | self.scan_amount_text = customtkinter.CTkEntry( 219 | self, 220 | validate="all", 221 | validatecommand=(self.int_validation, "%P", True), 222 | ) 223 | self.scan_amount_text.grid(row=4, column=1, padx=10, pady=(10, 0), sticky="ew") 224 | self.scan_amount_text.insert(0, str(config["scan"]["people_to_scan"])) 225 | 226 | output_values = [ 227 | { 228 | "name": "xlsx", 229 | "default": config["scan"]["formats"]["xlsx"], 230 | "group": "Output Format", 231 | }, 232 | { 233 | "name": "csv", 234 | "default": config["scan"]["formats"]["csv"], 235 | "group": "Output Format", 236 | }, 237 | { 238 | "name": "jsonl", 239 | "default": config["scan"]["formats"]["jsonl"], 240 | "group": "Output Format", 241 | }, 242 | ] 243 | self.output_options = HorizontalCheckboxFrame( 244 | self, output_values, "Output Format", 3 245 | ) 246 | self.output_options.grid( 247 | row=5, column=0, padx=10, pady=(5, 0), sticky="ew", columnspan=2 248 | ) 249 | 250 | def set_uuid(self, uuid): 251 | self.scan_uuid_var.set(uuid) 252 | 253 | def update_port(self, name=""): 254 | self.adb_port_text.delete(0, len(self.adb_port_text.get())) 255 | 256 | if name != "": 257 | self.adb_port_text.insert(0, get_bluestacks_port(name, self.config)) 258 | else: 259 | self.adb_port_text.insert( 260 | 0, get_bluestacks_port(self.bluestacks_instance_text.get(), self.config) 261 | ) 262 | return True 263 | 264 | def get_options(self): 265 | formats = OutputFormats() 266 | formats.from_dict(self.output_options.get()) 267 | return { 268 | "uuid": self.scan_uuid_var.get(), 269 | "name": self.scan_name_text.get(), 270 | "port": int(self.adb_port_text.get()), 271 | "amount": int(self.scan_amount_text.get()), 272 | "formats": formats, 273 | } 274 | 275 | def options_valid(self) -> bool: 276 | val_errors: List[str] = [] 277 | 278 | if not is_string_int(self.adb_port_text.get()): 279 | val_errors.append("Adb port invalid") 280 | 281 | if not is_string_int(self.scan_amount_text.get()): 282 | val_errors.append("People to scan invalid") 283 | 284 | if all(value == False for value in self.output_options.get().values()): 285 | val_errors.append("No output format checked") 286 | 287 | if len(val_errors) > 0: 288 | InfoDialog( 289 | "Invalid input", 290 | "\n".join(val_errors), 291 | f"200x{100 + len(val_errors) * 12}", 292 | ) 293 | 294 | name_valitation = sanitize_scanname(self.scan_name_text.get()) 295 | if not name_valitation.valid: 296 | InfoDialog( 297 | "Name is not valid", 298 | f"Name is not valid and got changed to:\n{name_valitation.result}\n" 299 | + f"Please check the new name and press start again.", 300 | f"400x{100 + 3 * 12}", 301 | ) 302 | self.scan_name_text.delete(0, customtkinter.END) 303 | self.scan_name_text.insert(0, name_valitation.result) 304 | 305 | return len(val_errors) == 0 and name_valitation.valid 306 | 307 | 308 | class AdditionalStatusInfo(customtkinter.CTkFrame): 309 | def __init__(self, master): 310 | super().__init__(master) 311 | self.values: Dict[str, customtkinter.StringVar] = {} 312 | self.grid_columnconfigure(0, weight=1) 313 | self.grid_columnconfigure(1, weight=1) 314 | self.grid_columnconfigure(2, weight=1) 315 | self.gov_number_var = customtkinter.StringVar(value="24 to 30 of 30") 316 | self.values.update({"govs": self.gov_number_var}) 317 | self.approx_time_remaining_var = customtkinter.StringVar(value="0:16:34") 318 | self.values.update({"eta": self.approx_time_remaining_var}) 319 | self.last_time_var = customtkinter.StringVar(value="13:55:30") 320 | self.values.update({"time": self.last_time_var}) 321 | 322 | self.last_time = customtkinter.CTkLabel(self, text="Current time", height=1) 323 | self.last_time.grid(row=0, column=0, padx=10, pady=5, sticky="w") 324 | 325 | self.last_time_text = customtkinter.CTkLabel( 326 | self, textvariable=self.last_time_var, height=1 327 | ) 328 | self.last_time_text.grid(row=1, column=0, padx=10, pady=5, sticky="w") 329 | 330 | self.eta = customtkinter.CTkLabel(self, text="ETA", height=1) 331 | self.eta.grid(row=0, column=2, padx=10, pady=5, sticky="e") 332 | self.time_remaining_text = customtkinter.CTkLabel( 333 | self, textvariable=self.approx_time_remaining_var, height=1 334 | ) 335 | self.time_remaining_text.grid(row=1, column=2, padx=10, pady=5, sticky="e") 336 | 337 | self.gov_number_text = customtkinter.CTkLabel( 338 | self, textvariable=self.gov_number_var, height=1 339 | ) 340 | self.gov_number_text.grid(row=0, column=1, pady=5, sticky="ew") 341 | 342 | def set_var(self, key, value): 343 | if key in self.values: 344 | self.values[key].set(value) 345 | 346 | 347 | class LastBatchInfo(customtkinter.CTkFrame): 348 | def __init__(self, master, govs_per_batch): 349 | super().__init__(master) 350 | self.grid_columnconfigure(0, weight=1) 351 | self.grid_columnconfigure(1, weight=1) 352 | self.entries: List[customtkinter.CTkLabel] = [] 353 | self.labels: List[customtkinter.CTkLabel] = [] 354 | self.variables: Dict[str, customtkinter.StringVar] = {} 355 | 356 | self.additional_stats = AdditionalStatusInfo(self) 357 | self.additional_stats.grid( 358 | row=0, column=0, columnspan=2, pady=(0, 5), sticky="ewsn" 359 | ) 360 | 361 | offset = 1 362 | 363 | for i in range(0, govs_per_batch): 364 | label_variable = customtkinter.StringVar(master=self, name=f"name-{i}") 365 | label = customtkinter.CTkLabel(self, textvariable=label_variable, height=1) 366 | entry_variable = customtkinter.StringVar(master=self, name=f"score-{i}") 367 | entry = customtkinter.CTkLabel(self, textvariable=entry_variable, height=1) 368 | 369 | label.grid( 370 | row=i + offset, # % ceil(len(values) / 2), 371 | column=0, 372 | padx=10, 373 | pady=2, 374 | sticky="w", 375 | ) 376 | entry.grid( 377 | row=i + offset, # % ceil(len(values) / 2), 378 | column=0 + 1, 379 | padx=(10, 30), 380 | pady=2, 381 | sticky="w", 382 | ) 383 | 384 | self.variables.update({f"name-{i}": label_variable}) 385 | self.variables.update({f"score-{i}": entry_variable}) 386 | self.labels.append(label) 387 | self.entries.append(entry) 388 | 389 | # Additional Info 390 | 391 | def set(self, values): 392 | for key, value in values.items(): 393 | if key in self.variables: 394 | if isinstance(value, int): 395 | self.variables[key].set(f"{value:,}") 396 | else: 397 | self.variables[key].set(value) 398 | else: 399 | self.additional_stats.set_var(key, value) 400 | 401 | 402 | class App(customtkinter.CTk): 403 | def __init__(self): 404 | super().__init__() 405 | 406 | file_validation = validate_installation() 407 | if not file_validation.success: 408 | self.withdraw() 409 | dia = InfoDialog( 410 | "Validation failed", 411 | "\n".join(file_validation.messages), 412 | "760x200", 413 | self.close_program, 414 | ) 415 | self.wait_window(dia) 416 | 417 | try: 418 | self.config = load_config() 419 | except ConfigError as e: 420 | logger.fatal(str(e)) 421 | dia = InfoDialog( 422 | "Invalid Config", 423 | str(e), 424 | "360x200", 425 | self.close_program, 426 | ) 427 | self.wait_window(dia) 428 | 429 | self.title("Honor Scanner by Cyrexxis") 430 | self.geometry("560x390") 431 | self.grid_columnconfigure(0, weight=4) 432 | self.grid_columnconfigure(1, weight=2) 433 | self.grid_rowconfigure(0, weight=1) 434 | 435 | self.options_frame = BasicOptionsFame(self, self.config) 436 | self.options_frame.grid(row=0, column=0, padx=10, pady=(10, 0), sticky="ewsn") 437 | 438 | self.last_batch_frame = LastBatchInfo(self, 5) 439 | 440 | self.last_batch_frame.set( 441 | { 442 | "name-0": "Super Governor 1", 443 | "score-0": "1000", 444 | "name-1": "Super Governor 2", 445 | "score-1": "500", 446 | "name-2": "Super Governor 3", 447 | "score-2": "250", 448 | "name-3": "Super Governor 4", 449 | "score-3": "125", 450 | "name-4": "Super Governor 5", 451 | "score-4": "64", 452 | } 453 | ) 454 | 455 | self.last_batch_frame.grid( 456 | row=0, column=1, padx=10, pady=(10, 10), sticky="ewsn", rowspan=2 457 | ) 458 | 459 | self.start_scan_button = customtkinter.CTkButton( 460 | self, text="Start scan", command=self.start_scan 461 | ) 462 | self.start_scan_button.grid(row=1, column=0, padx=10, pady=10, sticky="ew") 463 | 464 | self.end_scan_button = customtkinter.CTkButton( 465 | self, text="End scan", command=self.end_scan 466 | ) 467 | self.end_scan_button.grid(row=2, column=0, padx=10, pady=10, sticky="ew") 468 | 469 | self.current_state = customtkinter.CTkLabel(self, text="Not started", height=1) 470 | self.current_state.grid(row=2, column=1, padx=10, pady=(10, 0), sticky="ewns") 471 | 472 | def close_program(self): 473 | self.quit() 474 | 475 | def start_scan(self): 476 | Thread( 477 | target=self.launch_scanner, 478 | ).start() 479 | 480 | def launch_scanner(self): 481 | if not self.options_frame.options_valid(): 482 | return 483 | 484 | self.start_scan_button.configure(state="disabled") 485 | options = self.options_frame.get_options() 486 | 487 | try: 488 | self.honor_scanner = HonorScanner(options["port"], self.config) 489 | self.honor_scanner.set_batch_callback(self.governor_callback) 490 | self.honor_scanner.set_state_callback(self.state_callback) 491 | self.options_frame.set_uuid(self.honor_scanner.run_id) 492 | 493 | self.honor_scanner.start_scan( 494 | options["name"], 495 | options["amount"], 496 | options["formats"], 497 | ) 498 | except AdbError as error: 499 | logger.error( 500 | "An error with the adb connection occured (probably wrong port). Exact message: " 501 | + str(error) 502 | ) 503 | InfoDialog( 504 | "Error", 505 | "An error with the adb connection occured. Please verfiy that you use the correct port.\nExact message: " 506 | + str(error), 507 | "300x160", 508 | ) 509 | self.state_callback("Not started") 510 | finally: 511 | # Reset end scan button 512 | self.end_scan_button.configure(state="normal", text="End scan") 513 | self.start_scan_button.configure(state="normal") 514 | 515 | def end_scan(self): 516 | self.honor_scanner.end_scan() 517 | self.end_scan_button.configure( 518 | state="disabled", text="Abort after next governor" 519 | ) 520 | 521 | def governor_callback( 522 | self, gov_data: List[GovernorData], extra_data: AdditionalData 523 | ): 524 | # self.last_gov_frame.set(gov_info) 525 | 526 | batch_data: Dict[str, str | int] = {} 527 | for index, gov in enumerate(gov_data): 528 | batch_data.update({f"name-{index}": gov.name}) 529 | batch_data.update({f"score-{index}": to_int_or(gov.score, "Unknown")}) 530 | 531 | batch_data.update( 532 | { 533 | "govs": f"{extra_data.current_page * extra_data.govs_per_page} to {(extra_data.current_page + 1) * extra_data.govs_per_page} of {extra_data.target_governor}", 534 | "time": extra_data.current_time, 535 | "eta": extra_data.eta(), 536 | } 537 | ) 538 | 539 | self.last_batch_frame.set(batch_data) 540 | 541 | def state_callback(self, state): 542 | self.current_state.configure(text=state) 543 | 544 | 545 | app = App() 546 | app.report_callback_exception = ex_handler.handle_exception 547 | f = open(os.devnull, "w") 548 | sys.stdout = f 549 | sys.stderr = f 550 | app.mainloop() 551 | -------------------------------------------------------------------------------- /seed_scanner_ui.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dummy_root import get_app_root 3 | from roktracker.alliance.additional_data import AdditionalData 4 | from roktracker.alliance.governor_data import GovernorData 5 | from roktracker.seed.scanner import SeedScanner 6 | from roktracker.utils.check_python import check_py_version 7 | from roktracker.utils.exception_handling import GuiExceptionHandler 8 | from roktracker.utils.exceptions import AdbError, ConfigError 9 | from roktracker.utils.general import is_string_int, load_config 10 | from roktracker.utils.gui import InfoDialog 11 | from roktracker.utils.output_formats import OutputFormats 12 | 13 | logging.basicConfig( 14 | filename=str(get_app_root() / "seed-scanner.log"), 15 | encoding="utf-8", 16 | format="%(asctime)s %(module)s %(levelname)s %(message)s", 17 | level=logging.INFO, 18 | datefmt="%Y-%m-%d %H:%M:%S", 19 | ) 20 | 21 | check_py_version((3, 11)) 22 | 23 | import customtkinter 24 | import json 25 | import logging 26 | import os 27 | import sys 28 | import threading 29 | 30 | from dummy_root import get_app_root 31 | from roktracker.utils.validator import sanitize_scanname, validate_installation 32 | from roktracker.utils.adb import get_bluestacks_port 33 | from threading import ExceptHookArgs, Thread 34 | from typing import Dict, List 35 | 36 | 37 | logger = logging.getLogger(__name__) 38 | ex_handler = GuiExceptionHandler(logger) 39 | 40 | sys.excepthook = ex_handler.handle_exception 41 | threading.excepthook = ex_handler.handle_thread_exception 42 | 43 | customtkinter.set_appearance_mode( 44 | "system" 45 | ) # Modes: "System" (standard), "Dark", "Light" 46 | customtkinter.set_default_color_theme( 47 | "blue" 48 | ) # Themes: "blue" (standard), "green", "dark-blue" 49 | 50 | 51 | def to_int_or(element, alternative): 52 | if element == "Skipped": 53 | return element 54 | 55 | try: 56 | return int(element) 57 | except ValueError: 58 | return alternative 59 | 60 | 61 | class CheckboxFrame(customtkinter.CTkTabview): 62 | def __init__(self, master, values, groupName): 63 | super().__init__( 64 | master, 65 | state="disabled", 66 | width=0, 67 | height=0, 68 | segmented_button_fg_color=customtkinter.ThemeManager.theme["CTkFrame"][ 69 | "fg_color" 70 | ], 71 | segmented_button_selected_color=customtkinter.ThemeManager.theme[ 72 | "CTkFrame" 73 | ]["fg_color"], 74 | text_color_disabled=customtkinter.ThemeManager.theme["CTkLabel"][ 75 | "text_color" 76 | ], 77 | ) 78 | self.add(groupName) 79 | self.values = list(filter(lambda x: x["group"] == groupName, values)) # type: ignore 80 | self.checkboxes: List[customtkinter.CTkCheckBox] = [] 81 | 82 | for i, value in enumerate(self.values): 83 | checkbox = customtkinter.CTkCheckBox( 84 | self.tab(groupName), 85 | text=value["name"], 86 | onvalue=True, 87 | offvalue=False, 88 | checkbox_height=16, 89 | checkbox_width=16, 90 | height=16, 91 | ) 92 | checkbox.grid(row=i, column=0, padx=10, pady=2, sticky="w") 93 | 94 | if value["default"]: 95 | checkbox.select() 96 | 97 | self.checkboxes.append(checkbox) 98 | 99 | def get(self): 100 | values = {} 101 | for checkbox in self.checkboxes: 102 | values.update({checkbox.cget("text"): checkbox.get()}) 103 | return values 104 | 105 | 106 | class HorizontalCheckboxFrame(customtkinter.CTkTabview): 107 | def __init__(self, master, values, groupName, options_per_row): 108 | super().__init__( 109 | master, 110 | state="disabled", 111 | width=0, 112 | height=0, 113 | segmented_button_fg_color=customtkinter.ThemeManager.theme["CTkFrame"][ 114 | "fg_color" 115 | ], 116 | segmented_button_selected_color=customtkinter.ThemeManager.theme[ 117 | "CTkFrame" 118 | ]["fg_color"], 119 | text_color_disabled=customtkinter.ThemeManager.theme["CTkLabel"][ 120 | "text_color" 121 | ], 122 | ) 123 | self.add(groupName) 124 | self.values = list(filter(lambda x: x["group"] == groupName, values)) # type: ignore 125 | self.checkboxes: List[Dict[str, customtkinter.CTkCheckBox]] = [] 126 | 127 | for i in range(0, options_per_row): 128 | self.tab(groupName).columnconfigure(i, weight=1) 129 | 130 | cur_row = 0 131 | for i, value in enumerate(self.values): 132 | cur_col = i % options_per_row 133 | label = customtkinter.CTkLabel( 134 | self.tab(groupName), text=value["name"], height=1 135 | ) 136 | label.grid(row=cur_row, column=cur_col, padx=10, pady=2) 137 | 138 | checkbox = customtkinter.CTkCheckBox( 139 | self.tab(groupName), 140 | text="", 141 | onvalue=True, 142 | offvalue=False, 143 | checkbox_height=20, 144 | checkbox_width=20, 145 | height=20, 146 | width=20, 147 | ) 148 | checkbox.grid(row=cur_row + 1, column=cur_col, padx=10, pady=2) 149 | 150 | if value["default"]: 151 | checkbox.select() 152 | 153 | self.checkboxes.append({value["name"]: checkbox}) 154 | 155 | if (i + 1) % options_per_row == 0: 156 | cur_row += 2 157 | 158 | def get(self): 159 | values = {} 160 | for checkbox in self.checkboxes: 161 | for k, v in checkbox.items(): 162 | values.update({k: bool(v.get())}) 163 | return values 164 | 165 | 166 | class BasicOptionsFame(customtkinter.CTkFrame): 167 | def __init__(self, master, config): 168 | super().__init__(master) 169 | self.config = config 170 | 171 | self.int_validation = self.register(is_string_int) 172 | 173 | self.grid_columnconfigure(0, weight=1) 174 | self.grid_columnconfigure(1, weight=2) 175 | self.scan_uuid_label = customtkinter.CTkLabel(self, text="Scan UUID:", height=1) 176 | self.scan_uuid_label.grid(row=0, column=0, padx=10, pady=(10, 0), sticky="w") 177 | self.scan_uuid_var = customtkinter.StringVar(self, "---") 178 | self.scan_uuid_label_2 = customtkinter.CTkLabel( 179 | self, textvariable=self.scan_uuid_var, height=1, anchor="w" 180 | ) 181 | self.scan_uuid_label_2.grid(row=0, column=1, padx=10, pady=(10, 0), sticky="ew") 182 | 183 | self.scan_name_label = customtkinter.CTkLabel(self, text="Scan name:", height=1) 184 | self.scan_name_label.grid(row=1, column=0, padx=10, pady=(10, 0), sticky="w") 185 | self.scan_name_text = customtkinter.CTkEntry(self) 186 | self.scan_name_text.grid(row=1, column=1, padx=10, pady=(10, 0), sticky="ew") 187 | self.scan_name_text.insert(0, config["scan"]["kingdom_name"]) 188 | 189 | self.bluestacks_instance_label = customtkinter.CTkLabel( 190 | self, text="Bluestacks name:", height=1 191 | ) 192 | self.bluestacks_instance_label.grid( 193 | row=2, column=0, padx=10, pady=(10, 0), sticky="w" 194 | ) 195 | self.bluestacks_instance_text = customtkinter.CTkEntry(self) 196 | self.bluestacks_instance_text.grid( 197 | row=2, column=1, padx=10, pady=(10, 0), sticky="ew" 198 | ) 199 | self.bluestacks_instance_text.insert(0, config["general"]["bluestacks"]["name"]) 200 | 201 | self.adb_port_label = customtkinter.CTkLabel(self, text="Adb port:", height=1) 202 | self.adb_port_label.grid(row=3, column=0, padx=10, pady=(10, 0), sticky="w") 203 | self.adb_port_text = customtkinter.CTkEntry( 204 | self, 205 | validate="all", 206 | validatecommand=(self.int_validation, "%P", True), 207 | ) 208 | self.adb_port_text.grid(row=3, column=1, padx=10, pady=(10, 0), sticky="ew") 209 | self.bluestacks_instance_text.configure( 210 | validatecommand=(self.register(self.update_port), "%P"), validate="key" 211 | ) 212 | self.update_port() 213 | 214 | self.scan_amount_label = customtkinter.CTkLabel( 215 | self, text="People to scan:", height=1 216 | ) 217 | self.scan_amount_label.grid(row=4, column=0, padx=10, pady=(10, 0), sticky="w") 218 | self.scan_amount_text = customtkinter.CTkEntry( 219 | self, 220 | validate="all", 221 | validatecommand=(self.int_validation, "%P", True), 222 | ) 223 | self.scan_amount_text.grid(row=4, column=1, padx=10, pady=(10, 0), sticky="ew") 224 | self.scan_amount_text.insert(0, str(config["scan"]["people_to_scan"])) 225 | 226 | output_values = [ 227 | { 228 | "name": "xlsx", 229 | "default": config["scan"]["formats"]["xlsx"], 230 | "group": "Output Format", 231 | }, 232 | { 233 | "name": "csv", 234 | "default": config["scan"]["formats"]["csv"], 235 | "group": "Output Format", 236 | }, 237 | { 238 | "name": "jsonl", 239 | "default": config["scan"]["formats"]["jsonl"], 240 | "group": "Output Format", 241 | }, 242 | ] 243 | self.output_options = HorizontalCheckboxFrame( 244 | self, output_values, "Output Format", 3 245 | ) 246 | self.output_options.grid( 247 | row=5, column=0, padx=10, pady=(5, 0), sticky="ew", columnspan=2 248 | ) 249 | 250 | def set_uuid(self, uuid): 251 | self.scan_uuid_var.set(uuid) 252 | 253 | def update_port(self, name=""): 254 | self.adb_port_text.delete(0, len(self.adb_port_text.get())) 255 | 256 | if name != "": 257 | self.adb_port_text.insert(0, get_bluestacks_port(name, self.config)) 258 | else: 259 | self.adb_port_text.insert( 260 | 0, get_bluestacks_port(self.bluestacks_instance_text.get(), self.config) 261 | ) 262 | return True 263 | 264 | def get_options(self): 265 | formats = OutputFormats() 266 | formats.from_dict(self.output_options.get()) 267 | return { 268 | "uuid": self.scan_uuid_var.get(), 269 | "name": self.scan_name_text.get(), 270 | "port": int(self.adb_port_text.get()), 271 | "amount": int(self.scan_amount_text.get()), 272 | "formats": formats, 273 | } 274 | 275 | def options_valid(self) -> bool: 276 | val_errors: List[str] = [] 277 | 278 | if not is_string_int(self.adb_port_text.get()): 279 | val_errors.append("Adb port invalid") 280 | 281 | if not is_string_int(self.scan_amount_text.get()): 282 | val_errors.append("People to scan invalid") 283 | 284 | if all(value == False for value in self.output_options.get().values()): 285 | val_errors.append("No output format checked") 286 | 287 | if len(val_errors) > 0: 288 | InfoDialog( 289 | "Invalid input", 290 | "\n".join(val_errors), 291 | f"200x{100 + len(val_errors) * 12}", 292 | ) 293 | 294 | name_valitation = sanitize_scanname(self.scan_name_text.get()) 295 | if not name_valitation.valid: 296 | InfoDialog( 297 | "Name is not valid", 298 | f"Name is not valid and got changed to:\n{name_valitation.result}\n" 299 | + f"Please check the new name and press start again.", 300 | f"400x{100 + 3 * 12}", 301 | ) 302 | self.scan_name_text.delete(0, customtkinter.END) 303 | self.scan_name_text.insert(0, name_valitation.result) 304 | 305 | return len(val_errors) == 0 and name_valitation.valid 306 | 307 | 308 | class ScanOptionsFrame(customtkinter.CTkFrame): 309 | def __init__(self, master, values): 310 | super().__init__(master) 311 | self.grid_columnconfigure(0, weight=1) 312 | self.grid_rowconfigure(0, weight=1) 313 | self.grid_rowconfigure(1, weight=1) 314 | self.grid_rowconfigure(2, weight=1) 315 | self.values = values 316 | self.first_screen_options_frame = CheckboxFrame(self, values, "First Screen") 317 | self.second_screen_options_frame = CheckboxFrame(self, values, "Second Screen") 318 | self.third_screen_options_frame = CheckboxFrame(self, values, "Third Screen") 319 | 320 | self.first_screen_options_frame.grid( 321 | row=0, column=0, padx=10, pady=0, sticky="ewsn" 322 | ) 323 | 324 | self.second_screen_options_frame.grid( 325 | row=1, column=0, padx=10, pady=0, sticky="ewsn" 326 | ) 327 | 328 | self.third_screen_options_frame.grid( 329 | row=2, column=0, padx=10, pady=(0, 10), sticky="ewsn" 330 | ) 331 | 332 | def get(self): 333 | options = {} 334 | options.update(self.first_screen_options_frame.get()) 335 | options.update(self.second_screen_options_frame.get()) 336 | options.update(self.third_screen_options_frame.get()) 337 | return options 338 | 339 | 340 | class AdditionalStatusInfo(customtkinter.CTkFrame): 341 | def __init__(self, master): 342 | super().__init__(master) 343 | self.values: Dict[str, customtkinter.StringVar] = {} 344 | self.grid_columnconfigure(0, weight=1) 345 | self.grid_columnconfigure(1, weight=1) 346 | self.grid_columnconfigure(2, weight=1) 347 | self.gov_number_var = customtkinter.StringVar(value="24 to 30 of 30") 348 | self.values.update({"govs": self.gov_number_var}) 349 | self.approx_time_remaining_var = customtkinter.StringVar(value="0:16:34") 350 | self.values.update({"eta": self.approx_time_remaining_var}) 351 | self.last_time_var = customtkinter.StringVar(value="13:55:30") 352 | self.values.update({"time": self.last_time_var}) 353 | 354 | self.last_time = customtkinter.CTkLabel(self, text="Current time", height=1) 355 | self.last_time.grid(row=0, column=0, padx=10, pady=5, sticky="w") 356 | 357 | self.last_time_text = customtkinter.CTkLabel( 358 | self, textvariable=self.last_time_var, height=1 359 | ) 360 | self.last_time_text.grid(row=1, column=0, padx=10, pady=5, sticky="w") 361 | 362 | self.eta = customtkinter.CTkLabel(self, text="ETA", height=1) 363 | self.eta.grid(row=0, column=2, padx=10, pady=5, sticky="e") 364 | self.time_remaining_text = customtkinter.CTkLabel( 365 | self, textvariable=self.approx_time_remaining_var, height=1 366 | ) 367 | self.time_remaining_text.grid(row=1, column=2, padx=10, pady=5, sticky="e") 368 | 369 | self.gov_number_text = customtkinter.CTkLabel( 370 | self, textvariable=self.gov_number_var, height=1 371 | ) 372 | self.gov_number_text.grid(row=0, column=1, pady=5, sticky="ew") 373 | 374 | def set_var(self, key, value): 375 | if key in self.values: 376 | self.values[key].set(value) 377 | 378 | 379 | class LastBatchInfo(customtkinter.CTkFrame): 380 | def __init__(self, master, govs_per_batch): 381 | super().__init__(master) 382 | self.grid_columnconfigure(0, weight=1) 383 | self.grid_columnconfigure(1, weight=1) 384 | self.entries: List[customtkinter.CTkLabel] = [] 385 | self.labels: List[customtkinter.CTkLabel] = [] 386 | self.variables: Dict[str, customtkinter.StringVar] = {} 387 | 388 | self.additional_stats = AdditionalStatusInfo(self) 389 | self.additional_stats.grid( 390 | row=0, column=0, columnspan=2, pady=(0, 5), sticky="ewsn" 391 | ) 392 | 393 | offset = 1 394 | 395 | for i in range(0, govs_per_batch): 396 | label_variable = customtkinter.StringVar(master=self, name=f"name-{i}") 397 | label = customtkinter.CTkLabel(self, textvariable=label_variable, height=1) 398 | entry_variable = customtkinter.StringVar(master=self, name=f"score-{i}") 399 | entry = customtkinter.CTkLabel(self, textvariable=entry_variable, height=1) 400 | 401 | label.grid( 402 | row=i + offset, # % ceil(len(values) / 2), 403 | column=0, 404 | padx=10, 405 | pady=2, 406 | sticky="w", 407 | ) 408 | entry.grid( 409 | row=i + offset, # % ceil(len(values) / 2), 410 | column=0 + 1, 411 | padx=(10, 30), 412 | pady=2, 413 | sticky="w", 414 | ) 415 | 416 | self.variables.update({f"name-{i}": label_variable}) 417 | self.variables.update({f"score-{i}": entry_variable}) 418 | self.labels.append(label) 419 | self.entries.append(entry) 420 | 421 | # Additional Info 422 | 423 | def set(self, values): 424 | for key, value in values.items(): 425 | if key in self.variables: 426 | if isinstance(value, int): 427 | self.variables[key].set(f"{value:,}") 428 | else: 429 | self.variables[key].set(value) 430 | else: 431 | self.additional_stats.set_var(key, value) 432 | 433 | 434 | class App(customtkinter.CTk): 435 | def __init__(self): 436 | super().__init__() 437 | 438 | file_validation = validate_installation() 439 | if not file_validation.success: 440 | self.withdraw() 441 | dia = InfoDialog( 442 | "Validation failed", 443 | "\n".join(file_validation.messages), 444 | "760x200", 445 | self.close_program, 446 | ) 447 | self.wait_window(dia) 448 | 449 | try: 450 | self.config = load_config() 451 | except ConfigError as e: 452 | logger.fatal(str(e)) 453 | dia = InfoDialog( 454 | "Invalid Config", 455 | str(e), 456 | "360x200", 457 | self.close_program, 458 | ) 459 | self.wait_window(dia) 460 | 461 | self.title("Alliance Scanner by Cyrexxis") 462 | self.geometry("560x390") 463 | self.grid_columnconfigure(0, weight=4) 464 | self.grid_columnconfigure(1, weight=2) 465 | self.grid_rowconfigure(0, weight=1) 466 | 467 | self.options_frame = BasicOptionsFame(self, self.config) 468 | self.options_frame.grid(row=0, column=0, padx=10, pady=(10, 0), sticky="ewsn") 469 | 470 | self.last_batch_frame = LastBatchInfo(self, 6) 471 | 472 | self.last_batch_frame.set( 473 | { 474 | "name-0": "Super Governor 1", 475 | "score-0": "1000", 476 | "name-1": "Super Governor 2", 477 | "score-1": "500", 478 | "name-2": "Super Governor 3", 479 | "score-2": "250", 480 | "name-3": "Super Governor 4", 481 | "score-3": "125", 482 | "name-4": "Super Governor 5", 483 | "score-4": "64", 484 | "name-5": "Super Governor 6", 485 | "score-5": "32", 486 | } 487 | ) 488 | 489 | self.last_batch_frame.grid( 490 | row=0, column=1, padx=10, pady=(10, 10), sticky="ewsn", rowspan=2 491 | ) 492 | 493 | self.start_scan_button = customtkinter.CTkButton( 494 | self, text="Start scan", command=self.start_scan 495 | ) 496 | self.start_scan_button.grid(row=1, column=0, padx=10, pady=10, sticky="ew") 497 | 498 | self.end_scan_button = customtkinter.CTkButton( 499 | self, text="End scan", command=self.end_scan 500 | ) 501 | self.end_scan_button.grid(row=2, column=0, padx=10, pady=10, sticky="ew") 502 | 503 | self.current_state = customtkinter.CTkLabel(self, text="Not started", height=1) 504 | self.current_state.grid(row=2, column=1, padx=10, pady=(10, 0), sticky="ewns") 505 | 506 | def close_program(self): 507 | self.quit() 508 | 509 | def start_scan(self): 510 | Thread( 511 | target=self.launch_scanner, 512 | ).start() 513 | 514 | def launch_scanner(self): 515 | if not self.options_frame.options_valid(): 516 | return 517 | 518 | self.start_scan_button.configure(state="disabled") 519 | options = self.options_frame.get_options() 520 | 521 | try: 522 | self.alliance_scanner = SeedScanner(options["port"], self.config) 523 | self.alliance_scanner.set_batch_callback(self.governor_callback) 524 | self.alliance_scanner.set_state_callback(self.state_callback) 525 | self.options_frame.set_uuid(self.alliance_scanner.run_id) 526 | 527 | self.alliance_scanner.start_scan( 528 | options["name"], options["amount"], options["formats"] 529 | ) 530 | except AdbError as error: 531 | logger.error( 532 | "An error with the adb connection occured (probably wrong port). Exact message: " 533 | + str(error) 534 | ) 535 | InfoDialog( 536 | "Error", 537 | "An error with the adb connection occured. Please verfiy that you use the correct port.\nExact message: " 538 | + str(error), 539 | "300x160", 540 | ) 541 | self.state_callback("Not started") 542 | finally: 543 | # Reset end scan button 544 | self.end_scan_button.configure(state="normal", text="End scan") 545 | self.start_scan_button.configure(state="normal") 546 | 547 | def end_scan(self): 548 | self.alliance_scanner.end_scan() 549 | self.end_scan_button.configure( 550 | state="disabled", text="Abort after next governor" 551 | ) 552 | 553 | def governor_callback( 554 | self, gov_data: List[GovernorData], extra_data: AdditionalData 555 | ): 556 | # self.last_gov_frame.set(gov_info) 557 | 558 | batch_data: Dict[str, str | int] = {} 559 | for index, gov in enumerate(gov_data): 560 | batch_data.update({f"name-{index}": gov.name}) 561 | batch_data.update({f"score-{index}": to_int_or(gov.score, "Unknown")}) 562 | 563 | batch_data.update( 564 | { 565 | "govs": f"{extra_data.current_page * extra_data.govs_per_page} to {(extra_data.current_page + 1) * extra_data.govs_per_page} of {extra_data.target_governor}", 566 | "time": extra_data.current_time, 567 | "eta": extra_data.eta(), 568 | } 569 | ) 570 | 571 | self.last_batch_frame.set(batch_data) 572 | 573 | def state_callback(self, state): 574 | self.current_state.configure(text=state) 575 | 576 | 577 | app = App() 578 | app.report_callback_exception = ex_handler.handle_exception 579 | f = open(os.devnull, "w") 580 | sys.stdout = f 581 | sys.stderr = f 582 | app.mainloop() 583 | -------------------------------------------------------------------------------- /alliance_scanner_ui.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dummy_root import get_app_root 3 | from roktracker.alliance.additional_data import AdditionalData 4 | from roktracker.alliance.governor_data import GovernorData 5 | from roktracker.alliance.scanner import AllianceScanner 6 | from roktracker.utils.check_python import check_py_version 7 | from roktracker.utils.exception_handling import GuiExceptionHandler 8 | from roktracker.utils.exceptions import AdbError, ConfigError 9 | from roktracker.utils.general import is_string_int, load_config 10 | from roktracker.utils.gui import InfoDialog 11 | from roktracker.utils.output_formats import OutputFormats 12 | 13 | logging.basicConfig( 14 | filename=str(get_app_root() / "alliance-scanner.log"), 15 | encoding="utf-8", 16 | format="%(asctime)s %(module)s %(levelname)s %(message)s", 17 | level=logging.INFO, 18 | datefmt="%Y-%m-%d %H:%M:%S", 19 | ) 20 | 21 | check_py_version((3, 11)) 22 | 23 | import customtkinter 24 | import json 25 | import logging 26 | import os 27 | import sys 28 | import threading 29 | 30 | from dummy_root import get_app_root 31 | from roktracker.utils.validator import sanitize_scanname, validate_installation 32 | from roktracker.utils.adb import get_bluestacks_port 33 | from threading import ExceptHookArgs, Thread 34 | from typing import Dict, List 35 | 36 | 37 | logger = logging.getLogger(__name__) 38 | ex_handler = GuiExceptionHandler(logger) 39 | 40 | sys.excepthook = ex_handler.handle_exception 41 | threading.excepthook = ex_handler.handle_thread_exception 42 | 43 | customtkinter.set_appearance_mode( 44 | "system" 45 | ) # Modes: "System" (standard), "Dark", "Light" 46 | customtkinter.set_default_color_theme( 47 | "blue" 48 | ) # Themes: "blue" (standard), "green", "dark-blue" 49 | 50 | 51 | def to_int_or(element, alternative): 52 | if element == "Skipped": 53 | return element 54 | 55 | try: 56 | return int(element) 57 | except ValueError: 58 | return alternative 59 | 60 | 61 | class CheckboxFrame(customtkinter.CTkTabview): 62 | def __init__(self, master, values, groupName): 63 | super().__init__( 64 | master, 65 | state="disabled", 66 | width=0, 67 | height=0, 68 | segmented_button_fg_color=customtkinter.ThemeManager.theme["CTkFrame"][ 69 | "fg_color" 70 | ], 71 | segmented_button_selected_color=customtkinter.ThemeManager.theme[ 72 | "CTkFrame" 73 | ]["fg_color"], 74 | text_color_disabled=customtkinter.ThemeManager.theme["CTkLabel"][ 75 | "text_color" 76 | ], 77 | ) 78 | self.add(groupName) 79 | self.values = list(filter(lambda x: x["group"] == groupName, values)) # type: ignore 80 | self.checkboxes: List[customtkinter.CTkCheckBox] = [] 81 | 82 | for i, value in enumerate(self.values): 83 | checkbox = customtkinter.CTkCheckBox( 84 | self.tab(groupName), 85 | text=value["name"], 86 | onvalue=True, 87 | offvalue=False, 88 | checkbox_height=16, 89 | checkbox_width=16, 90 | height=16, 91 | ) 92 | checkbox.grid(row=i, column=0, padx=10, pady=2, sticky="w") 93 | 94 | if value["default"]: 95 | checkbox.select() 96 | 97 | self.checkboxes.append(checkbox) 98 | 99 | def get(self): 100 | values = {} 101 | for checkbox in self.checkboxes: 102 | values.update({checkbox.cget("text"): checkbox.get()}) 103 | return values 104 | 105 | 106 | class HorizontalCheckboxFrame(customtkinter.CTkTabview): 107 | def __init__(self, master, values, groupName, options_per_row): 108 | super().__init__( 109 | master, 110 | state="disabled", 111 | width=0, 112 | height=0, 113 | segmented_button_fg_color=customtkinter.ThemeManager.theme["CTkFrame"][ 114 | "fg_color" 115 | ], 116 | segmented_button_selected_color=customtkinter.ThemeManager.theme[ 117 | "CTkFrame" 118 | ]["fg_color"], 119 | text_color_disabled=customtkinter.ThemeManager.theme["CTkLabel"][ 120 | "text_color" 121 | ], 122 | ) 123 | self.add(groupName) 124 | self.values = list(filter(lambda x: x["group"] == groupName, values)) # type: ignore 125 | self.checkboxes: List[Dict[str, customtkinter.CTkCheckBox]] = [] 126 | 127 | for i in range(0, options_per_row): 128 | self.tab(groupName).columnconfigure(i, weight=1) 129 | 130 | cur_row = 0 131 | for i, value in enumerate(self.values): 132 | cur_col = i % options_per_row 133 | label = customtkinter.CTkLabel( 134 | self.tab(groupName), text=value["name"], height=1 135 | ) 136 | label.grid(row=cur_row, column=cur_col, padx=10, pady=2) 137 | 138 | checkbox = customtkinter.CTkCheckBox( 139 | self.tab(groupName), 140 | text="", 141 | onvalue=True, 142 | offvalue=False, 143 | checkbox_height=20, 144 | checkbox_width=20, 145 | height=20, 146 | width=20, 147 | ) 148 | checkbox.grid(row=cur_row + 1, column=cur_col, padx=10, pady=2) 149 | 150 | if value["default"]: 151 | checkbox.select() 152 | 153 | self.checkboxes.append({value["name"]: checkbox}) 154 | 155 | if (i + 1) % options_per_row == 0: 156 | cur_row += 2 157 | 158 | def get(self): 159 | values = {} 160 | for checkbox in self.checkboxes: 161 | for k, v in checkbox.items(): 162 | values.update({k: bool(v.get())}) 163 | return values 164 | 165 | 166 | class BasicOptionsFame(customtkinter.CTkFrame): 167 | def __init__(self, master, config): 168 | super().__init__(master) 169 | self.config = config 170 | 171 | self.int_validation = self.register(is_string_int) 172 | 173 | self.grid_columnconfigure(0, weight=1) 174 | self.grid_columnconfigure(1, weight=2) 175 | self.scan_uuid_label = customtkinter.CTkLabel(self, text="Scan UUID:", height=1) 176 | self.scan_uuid_label.grid(row=0, column=0, padx=10, pady=(10, 0), sticky="w") 177 | self.scan_uuid_var = customtkinter.StringVar(self, "---") 178 | self.scan_uuid_label_2 = customtkinter.CTkLabel( 179 | self, textvariable=self.scan_uuid_var, height=1, anchor="w" 180 | ) 181 | self.scan_uuid_label_2.grid(row=0, column=1, padx=10, pady=(10, 0), sticky="ew") 182 | 183 | self.scan_name_label = customtkinter.CTkLabel(self, text="Scan name:", height=1) 184 | self.scan_name_label.grid(row=1, column=0, padx=10, pady=(10, 0), sticky="w") 185 | self.scan_name_text = customtkinter.CTkEntry(self) 186 | self.scan_name_text.grid(row=1, column=1, padx=10, pady=(10, 0), sticky="ew") 187 | self.scan_name_text.insert(0, config["scan"]["kingdom_name"]) 188 | 189 | self.bluestacks_instance_label = customtkinter.CTkLabel( 190 | self, text="Bluestacks name:", height=1 191 | ) 192 | self.bluestacks_instance_label.grid( 193 | row=2, column=0, padx=10, pady=(10, 0), sticky="w" 194 | ) 195 | self.bluestacks_instance_text = customtkinter.CTkEntry(self) 196 | self.bluestacks_instance_text.grid( 197 | row=2, column=1, padx=10, pady=(10, 0), sticky="ew" 198 | ) 199 | self.bluestacks_instance_text.insert(0, config["general"]["bluestacks"]["name"]) 200 | 201 | self.adb_port_label = customtkinter.CTkLabel(self, text="Adb port:", height=1) 202 | self.adb_port_label.grid(row=3, column=0, padx=10, pady=(10, 0), sticky="w") 203 | self.adb_port_text = customtkinter.CTkEntry( 204 | self, 205 | validate="all", 206 | validatecommand=(self.int_validation, "%P", True), 207 | ) 208 | self.adb_port_text.grid(row=3, column=1, padx=10, pady=(10, 0), sticky="ew") 209 | self.bluestacks_instance_text.configure( 210 | validatecommand=(self.register(self.update_port), "%P"), validate="key" 211 | ) 212 | self.update_port() 213 | 214 | self.scan_amount_label = customtkinter.CTkLabel( 215 | self, text="People to scan:", height=1 216 | ) 217 | self.scan_amount_label.grid(row=4, column=0, padx=10, pady=(10, 0), sticky="w") 218 | self.scan_amount_text = customtkinter.CTkEntry( 219 | self, 220 | validate="all", 221 | validatecommand=(self.int_validation, "%P", True), 222 | ) 223 | self.scan_amount_text.grid(row=4, column=1, padx=10, pady=(10, 0), sticky="ew") 224 | self.scan_amount_text.insert(0, str(config["scan"]["people_to_scan"])) 225 | 226 | output_values = [ 227 | { 228 | "name": "xlsx", 229 | "default": config["scan"]["formats"]["xlsx"], 230 | "group": "Output Format", 231 | }, 232 | { 233 | "name": "csv", 234 | "default": config["scan"]["formats"]["csv"], 235 | "group": "Output Format", 236 | }, 237 | { 238 | "name": "jsonl", 239 | "default": config["scan"]["formats"]["jsonl"], 240 | "group": "Output Format", 241 | }, 242 | ] 243 | self.output_options = HorizontalCheckboxFrame( 244 | self, output_values, "Output Format", 3 245 | ) 246 | self.output_options.grid( 247 | row=5, column=0, padx=10, pady=(5, 0), sticky="ew", columnspan=2 248 | ) 249 | 250 | def set_uuid(self, uuid): 251 | self.scan_uuid_var.set(uuid) 252 | 253 | def update_port(self, name=""): 254 | self.adb_port_text.delete(0, len(self.adb_port_text.get())) 255 | 256 | if name != "": 257 | self.adb_port_text.insert(0, get_bluestacks_port(name, self.config)) 258 | else: 259 | self.adb_port_text.insert( 260 | 0, get_bluestacks_port(self.bluestacks_instance_text.get(), self.config) 261 | ) 262 | return True 263 | 264 | def get_options(self): 265 | formats = OutputFormats() 266 | formats.from_dict(self.output_options.get()) 267 | return { 268 | "uuid": self.scan_uuid_var.get(), 269 | "name": self.scan_name_text.get(), 270 | "port": int(self.adb_port_text.get()), 271 | "amount": int(self.scan_amount_text.get()), 272 | "formats": formats, 273 | } 274 | 275 | def options_valid(self) -> bool: 276 | val_errors: List[str] = [] 277 | 278 | if not is_string_int(self.adb_port_text.get()): 279 | val_errors.append("Adb port invalid") 280 | 281 | if not is_string_int(self.scan_amount_text.get()): 282 | val_errors.append("People to scan invalid") 283 | 284 | if all(value == False for value in self.output_options.get().values()): 285 | val_errors.append("No output format checked") 286 | 287 | if len(val_errors) > 0: 288 | InfoDialog( 289 | "Invalid input", 290 | "\n".join(val_errors), 291 | f"200x{100 + len(val_errors) * 12}", 292 | ) 293 | 294 | name_valitation = sanitize_scanname(self.scan_name_text.get()) 295 | if not name_valitation.valid: 296 | InfoDialog( 297 | "Name is not valid", 298 | f"Name is not valid and got changed to:\n{name_valitation.result}\n" 299 | + f"Please check the new name and press start again.", 300 | f"400x{100 + 3 * 12}", 301 | ) 302 | self.scan_name_text.delete(0, customtkinter.END) 303 | self.scan_name_text.insert(0, name_valitation.result) 304 | 305 | return len(val_errors) == 0 and name_valitation.valid 306 | 307 | 308 | class ScanOptionsFrame(customtkinter.CTkFrame): 309 | def __init__(self, master, values): 310 | super().__init__(master) 311 | self.grid_columnconfigure(0, weight=1) 312 | self.grid_rowconfigure(0, weight=1) 313 | self.grid_rowconfigure(1, weight=1) 314 | self.grid_rowconfigure(2, weight=1) 315 | self.values = values 316 | self.first_screen_options_frame = CheckboxFrame(self, values, "First Screen") 317 | self.second_screen_options_frame = CheckboxFrame(self, values, "Second Screen") 318 | self.third_screen_options_frame = CheckboxFrame(self, values, "Third Screen") 319 | 320 | self.first_screen_options_frame.grid( 321 | row=0, column=0, padx=10, pady=0, sticky="ewsn" 322 | ) 323 | 324 | self.second_screen_options_frame.grid( 325 | row=1, column=0, padx=10, pady=0, sticky="ewsn" 326 | ) 327 | 328 | self.third_screen_options_frame.grid( 329 | row=2, column=0, padx=10, pady=(0, 10), sticky="ewsn" 330 | ) 331 | 332 | def get(self): 333 | options = {} 334 | options.update(self.first_screen_options_frame.get()) 335 | options.update(self.second_screen_options_frame.get()) 336 | options.update(self.third_screen_options_frame.get()) 337 | return options 338 | 339 | 340 | class AdditionalStatusInfo(customtkinter.CTkFrame): 341 | def __init__(self, master): 342 | super().__init__(master) 343 | self.values: Dict[str, customtkinter.StringVar] = {} 344 | self.grid_columnconfigure(0, weight=1) 345 | self.grid_columnconfigure(1, weight=1) 346 | self.grid_columnconfigure(2, weight=1) 347 | self.gov_number_var = customtkinter.StringVar(value="24 to 30 of 30") 348 | self.values.update({"govs": self.gov_number_var}) 349 | self.approx_time_remaining_var = customtkinter.StringVar(value="0:16:34") 350 | self.values.update({"eta": self.approx_time_remaining_var}) 351 | self.last_time_var = customtkinter.StringVar(value="13:55:30") 352 | self.values.update({"time": self.last_time_var}) 353 | 354 | self.last_time = customtkinter.CTkLabel(self, text="Current time", height=1) 355 | self.last_time.grid(row=0, column=0, padx=10, pady=5, sticky="w") 356 | 357 | self.last_time_text = customtkinter.CTkLabel( 358 | self, textvariable=self.last_time_var, height=1 359 | ) 360 | self.last_time_text.grid(row=1, column=0, padx=10, pady=5, sticky="w") 361 | 362 | self.eta = customtkinter.CTkLabel(self, text="ETA", height=1) 363 | self.eta.grid(row=0, column=2, padx=10, pady=5, sticky="e") 364 | self.time_remaining_text = customtkinter.CTkLabel( 365 | self, textvariable=self.approx_time_remaining_var, height=1 366 | ) 367 | self.time_remaining_text.grid(row=1, column=2, padx=10, pady=5, sticky="e") 368 | 369 | self.gov_number_text = customtkinter.CTkLabel( 370 | self, textvariable=self.gov_number_var, height=1 371 | ) 372 | self.gov_number_text.grid(row=0, column=1, pady=5, sticky="ew") 373 | 374 | def set_var(self, key, value): 375 | if key in self.values: 376 | self.values[key].set(value) 377 | 378 | 379 | class LastBatchInfo(customtkinter.CTkFrame): 380 | def __init__(self, master, govs_per_batch): 381 | super().__init__(master) 382 | self.grid_columnconfigure(0, weight=1) 383 | self.grid_columnconfigure(1, weight=1) 384 | self.entries: List[customtkinter.CTkLabel] = [] 385 | self.labels: List[customtkinter.CTkLabel] = [] 386 | self.variables: Dict[str, customtkinter.StringVar] = {} 387 | 388 | self.additional_stats = AdditionalStatusInfo(self) 389 | self.additional_stats.grid( 390 | row=0, column=0, columnspan=2, pady=(0, 5), sticky="ewsn" 391 | ) 392 | 393 | offset = 1 394 | 395 | for i in range(0, govs_per_batch): 396 | label_variable = customtkinter.StringVar(master=self, name=f"name-{i}") 397 | label = customtkinter.CTkLabel(self, textvariable=label_variable, height=1) 398 | entry_variable = customtkinter.StringVar(master=self, name=f"score-{i}") 399 | entry = customtkinter.CTkLabel(self, textvariable=entry_variable, height=1) 400 | 401 | label.grid( 402 | row=i + offset, # % ceil(len(values) / 2), 403 | column=0, 404 | padx=10, 405 | pady=2, 406 | sticky="w", 407 | ) 408 | entry.grid( 409 | row=i + offset, # % ceil(len(values) / 2), 410 | column=0 + 1, 411 | padx=(10, 30), 412 | pady=2, 413 | sticky="w", 414 | ) 415 | 416 | self.variables.update({f"name-{i}": label_variable}) 417 | self.variables.update({f"score-{i}": entry_variable}) 418 | self.labels.append(label) 419 | self.entries.append(entry) 420 | 421 | # Additional Info 422 | 423 | def set(self, values): 424 | for key, value in values.items(): 425 | if key in self.variables: 426 | if isinstance(value, int): 427 | self.variables[key].set(f"{value:,}") 428 | else: 429 | self.variables[key].set(value) 430 | else: 431 | self.additional_stats.set_var(key, value) 432 | 433 | 434 | class App(customtkinter.CTk): 435 | def __init__(self): 436 | super().__init__() 437 | 438 | file_validation = validate_installation() 439 | if not file_validation.success: 440 | self.withdraw() 441 | dia = InfoDialog( 442 | "Validation failed", 443 | "\n".join(file_validation.messages), 444 | "760x200", 445 | self.close_program, 446 | ) 447 | self.wait_window(dia) 448 | 449 | try: 450 | self.config = load_config() 451 | except ConfigError as e: 452 | logger.fatal(str(e)) 453 | dia = InfoDialog( 454 | "Invalid Config", 455 | str(e), 456 | "360x200", 457 | self.close_program, 458 | ) 459 | self.wait_window(dia) 460 | 461 | self.title("Alliance Scanner by Cyrexxis") 462 | self.geometry("560x390") 463 | self.grid_columnconfigure(0, weight=4) 464 | self.grid_columnconfigure(1, weight=2) 465 | self.grid_rowconfigure(0, weight=1) 466 | 467 | self.options_frame = BasicOptionsFame(self, self.config) 468 | self.options_frame.grid(row=0, column=0, padx=10, pady=(10, 0), sticky="ewsn") 469 | 470 | self.last_batch_frame = LastBatchInfo(self, 6) 471 | 472 | self.last_batch_frame.set( 473 | { 474 | "name-0": "Super Governor 1", 475 | "score-0": "1000", 476 | "name-1": "Super Governor 2", 477 | "score-1": "500", 478 | "name-2": "Super Governor 3", 479 | "score-2": "250", 480 | "name-3": "Super Governor 4", 481 | "score-3": "125", 482 | "name-4": "Super Governor 5", 483 | "score-4": "64", 484 | "name-5": "Super Governor 6", 485 | "score-5": "32", 486 | } 487 | ) 488 | 489 | self.last_batch_frame.grid( 490 | row=0, column=1, padx=10, pady=(10, 10), sticky="ewsn", rowspan=2 491 | ) 492 | 493 | self.start_scan_button = customtkinter.CTkButton( 494 | self, text="Start scan", command=self.start_scan 495 | ) 496 | self.start_scan_button.grid(row=1, column=0, padx=10, pady=10, sticky="ew") 497 | 498 | self.end_scan_button = customtkinter.CTkButton( 499 | self, text="End scan", command=self.end_scan 500 | ) 501 | self.end_scan_button.grid(row=2, column=0, padx=10, pady=10, sticky="ew") 502 | 503 | self.current_state = customtkinter.CTkLabel(self, text="Not started", height=1) 504 | self.current_state.grid(row=2, column=1, padx=10, pady=(10, 0), sticky="ewns") 505 | 506 | def close_program(self): 507 | self.quit() 508 | 509 | def start_scan(self): 510 | Thread( 511 | target=self.launch_scanner, 512 | ).start() 513 | 514 | def launch_scanner(self): 515 | if not self.options_frame.options_valid(): 516 | return 517 | 518 | self.start_scan_button.configure(state="disabled") 519 | options = self.options_frame.get_options() 520 | 521 | try: 522 | self.alliance_scanner = AllianceScanner(options["port"], self.config) 523 | self.alliance_scanner.set_batch_callback(self.governor_callback) 524 | self.alliance_scanner.set_state_callback(self.state_callback) 525 | self.options_frame.set_uuid(self.alliance_scanner.run_id) 526 | 527 | self.alliance_scanner.start_scan( 528 | options["name"], options["amount"], options["formats"] 529 | ) 530 | except AdbError as error: 531 | logger.error( 532 | "An error with the adb connection occured (probably wrong port). Exact message: " 533 | + str(error) 534 | ) 535 | InfoDialog( 536 | "Error", 537 | "An error with the adb connection occured. Please verfiy that you use the correct port.\nExact message: " 538 | + str(error), 539 | "300x160", 540 | ) 541 | self.state_callback("Not started") 542 | finally: 543 | # Reset end scan button 544 | self.end_scan_button.configure(state="normal", text="End scan") 545 | self.start_scan_button.configure(state="normal") 546 | 547 | def end_scan(self): 548 | self.alliance_scanner.end_scan() 549 | self.end_scan_button.configure( 550 | state="disabled", text="Abort after next governor" 551 | ) 552 | 553 | def governor_callback( 554 | self, gov_data: List[GovernorData], extra_data: AdditionalData 555 | ): 556 | # self.last_gov_frame.set(gov_info) 557 | 558 | batch_data: Dict[str, str | int] = {} 559 | for index, gov in enumerate(gov_data): 560 | batch_data.update({f"name-{index}": gov.name}) 561 | batch_data.update({f"score-{index}": to_int_or(gov.score, "Unknown")}) 562 | 563 | batch_data.update( 564 | { 565 | "govs": f"{extra_data.current_page * extra_data.govs_per_page} to {(extra_data.current_page + 1) * extra_data.govs_per_page} of {extra_data.target_governor}", 566 | "time": extra_data.current_time, 567 | "eta": extra_data.eta(), 568 | } 569 | ) 570 | 571 | self.last_batch_frame.set(batch_data) 572 | 573 | def state_callback(self, state): 574 | self.current_state.configure(text=state) 575 | 576 | 577 | app = App() 578 | app.report_callback_exception = ex_handler.handle_exception 579 | f = open(os.devnull, "w") 580 | sys.stdout = f 581 | sys.stderr = f 582 | app.mainloop() 583 | --------------------------------------------------------------------------------