Showing preview only (231K chars total). Download the full file or copy to clipboard to get everything.
Repository: ReZeroE/StarRail
Branch: main
Commit: 59830ce63768
Files: 59
Total size: 214.2 KB
Directory structure:
gitextract_b305clb7/
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── pyproject.toml
├── requirements.txt
├── setup.cfg
├── setup.py
├── src/
│ └── starrail/
│ ├── __init__.py
│ ├── automation/
│ │ ├── __init__.py
│ │ ├── config/
│ │ │ ├── __init__.py
│ │ │ └── automation_config_handler.py
│ │ ├── pixel_calculator/
│ │ │ ├── __init__.py
│ │ │ ├── pixel_calculator.py
│ │ │ └── resolution_detector.py
│ │ ├── recorder.py
│ │ └── units/
│ │ ├── __init__.py
│ │ ├── action.py
│ │ └── sequence.py
│ ├── bin/
│ │ ├── __init__.py
│ │ ├── loader/
│ │ │ ├── __init__.py
│ │ │ └── loader.py
│ │ ├── logs/
│ │ │ └── starrail_log.txt
│ │ ├── pick/
│ │ │ ├── __init__.py
│ │ │ └── pick.py
│ │ ├── pid/
│ │ │ └── get_active_pid.c
│ │ └── scheduler/
│ │ ├── __init__.py
│ │ ├── config/
│ │ │ ├── __init__.py
│ │ │ └── starrail_schedule_config.py
│ │ └── starrail_scheduler.py
│ ├── config/
│ │ ├── __init__.py
│ │ └── config_handler.py
│ ├── constants.py
│ ├── controllers/
│ │ ├── __init__.py
│ │ ├── automation_controller.py
│ │ ├── c_click_controller.py
│ │ ├── star_rail_app.py
│ │ ├── streaming_assets_controller.py
│ │ ├── web_controller.py
│ │ └── webcache_controller.py
│ ├── data/
│ │ └── textfiles/
│ │ ├── disclaimer.txt
│ │ └── webcache_explain.txt
│ ├── entrypoints/
│ │ ├── __init__.py
│ │ ├── entrypoint_handler.py
│ │ ├── entrypoints.py
│ │ └── help_format_handler.py
│ ├── exceptions/
│ │ ├── __init__.py
│ │ └── exceptions.py
│ └── utils/
│ ├── __init__.py
│ ├── binary_decoder.py
│ ├── game_detector.py
│ ├── json_handler.py
│ ├── perm_elevate.py
│ ├── process_handler.py
│ └── utils.py
└── tests/
├── test_starrail.py
└── verify_package.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Ignore existing configs
src/starrail/config/starrail_config.json
src/starrail/bin/scheduler/config/*.json
# Local Screenshots
src/starrail/_data/images/screenshots/*.png
# Temp SIFT Test Images
src/starrail/_utils/cv2_SIFT/tmp_data
# Incomplete UI scripts
src/starrail/user_interface/ui*.py
# Temp Test Scritps
tests/tmp*test.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
================================================
FILE: CHANGELOG.md
================================================
# Change Log
### Version 1.0.5 - [9/25/2024]
- Added feature to show live game status (`starrail status --live`).
- Various minor bug fixes and code optimizations.
### Version 1.0.4 - [9/4/2024]
- Added the following quick-links to the web controller
- BiliBili (CN) `> starrail bilibili`
- Homepage (CN) `> starrail homepage -cn`
- Various minor bug fixes and code optimizations.
### Version 1.0.3, 1.0.4 - [8/29/2024]
- Fixed more compatibility issues with the updated game directory structure.
- Improved error messaging for the `starrail` scheduler and automation handler.
- Introduced a handler to manage case when web cache binary file are locked when the game is active.
- Updated the automation section in the README file to use `automation remove` instead of `automation delete`.
- Various minor bug fixes and code optimizations.
### Version 1.0.2 - [8/27/2024]
- Resolved issue with auto-detecting the game's executable's path.
- New HoyoPlay launcher introduced a new game directory structure that broke old game detecting system. Package now matches:
1. `.../Star Rail/Game/StarRail.exe`
2. `.../Star Rail Games/StarRail.exe`
- Implemented new "weak match" feature to future-proof another change in game directory structure.
- Added new error message if package is unable to locate the game.
### Version 1.0.1 - [8/26/2024]
- Resolved version support issues (now supports Python 3.7 and later).
### Version 1.0.0 - [8/1/2024]
1. Complete redesign of the entire project.
- [Package Info](https://github.com/ReZeroE/StarRail/wiki/3.-Package-Information)
- About Package
- Version
- Author
- Repository
- [CLI Launcher & Scheduler](https://github.com/ReZeroE/StarRail/wiki/4.-Start-Stop-&-Schedule-Game)
- Start Game
- Stop Game
- Schedule Start/Stop
- [Game Configuration](https://github.com/ReZeroE/StarRail/wiki/5.-Game-Configurations)
- Real-time Game Status
- Base Game Information
- Detailed Client Information
- [Simple Automation](https://github.com/ReZeroE/StarRail/wiki/6.-Simple-Automation)
- Custom Automation
- Uniform Clicks
- [Official Page Access](https://github.com/ReZeroE/StarRail/wiki/7.-Official-Page-Access)
- Official Homepage
- Official Youtube Page
- Official HoyoLab Page
- [Binary Utilities](https://github.com/ReZeroE/StarRail/wiki/8.-Binary-Utilities)
- Decoded Web Cache (events, pulls, announcements)
- Cached Pull History
- Supplementary Binary Decoder
- [Misc Utilities](https://github.com/ReZeroE/StarRail/wiki/9.-Misc-Utilities)
- View Screenshots
- View Game Logs
<br/>
Please view the [developer's note](https://github.com/ReZeroE/StarRail/wiki/99.-Developer's-Note) for more information regarding the project overhaul.
<br/>
***
<br/>
## Legacy Version Logs
### Version 0.0.3 - [6/7/2023]
1. Optimized `starrail configure` to use multiprocessing Managers (speedup in local game search).
2. Added feature to downscale screen feature matching threshold based on the native screen resolution.
- Enabled `starrail` to support 4K, 2K, 1080P or lower resolution screens.
3. Added logout feature.
### Version 0.0.3 - [6/4/2023]
1. Added new rewards logic maps (Daily Training, Assignments)
2. Restructured the code framework of logic maps for better optimization.
3. The starrail show-config command now displays the absolute path of the game executable after configuration.
4. Adjusted image feature matching values to allow for less accurate matches in specific circumstances.
5. The time delay following a simulated mouse or keyboard key click has been extended.
### Version 0.0.3 - [6/3/2023]
1. Added Logic Map for Calyx Golden (bud_of_memories, bud_of_aether, bud_of_threasures)
2. Tested automation features for login, reward collection, and Calyx Golden.
3. Implemented "secondary image detection" with SIFT and FLANN for non-centered buttons (non-centered buttons were previously tracked with pixels offsets (x, y)).
### Version 0.0.3 - [6/1/2023]
1. Implemented "Logic Maps" structures (process sequence maps for automation).
2. Implemented base wrapper classes for auto grind(Calyx), reward collection, and login that utilizes the Logic Maps for automation.
3. Added Logic Maps for login and reward collection.
4. Updated project code structure.
### Version 0.0.3 - [5/21/2023]
1. Implemented SIFT (Scale Invariant Feature Transform) algorithm for feature detection and description, and FLANN (Fast Library for Approximate Nearest Neighbors) for feature matching. This is used to auto-detect buttons on-screen for executing process sequences.
2. Implemented RANSAC for finding homography to account for any scale, rotation or translation between the images to support various game window sizes (4k, 2k, 1080p, etc).
### Version 0.0.3 - [5/10/2023]
1. Optimized the `starrail configure` process to use multithreading when searching for the local game instance (Honkai: Star Rail) for a decrease in runtime.
### Version 0.0.3 - [5/1/2023]
1. Removed faulty dependencies that cannot be properly installed from PyPI
2. Resolved game path auto-detection issue
### Version 0.0.2 - [4/30/2023]
1. Stablized commandline features for start, stop, configure
2. Added commandline feature for overwriting previous path configurations:
```shell
$ starrail set-path
```
3. And other efficiency and maintainability related optimizations
### Version 0.0.2 - [4/29/2023]
Added commandline support for the following operations (**UNSTABLE**):
1. Configure `starrail` (only once after download):
```shell
$ starrail configure
```
2. Starting Honkai: Star Rail from the commandline
```shell
$ starrail start
```
3. Terminating Honkai: Star Rail from the commandline (started from `starrail`)
```shell
$ starrail stop
```
### Version 0.0.1 - [4/26/2023]
Initial Release
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
_Updated on 7/1/2024_
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at kevinliu@vt.edu. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Kevin L.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MANIFEST.in
================================================
recursive-include src/starrail *.json *.png *.jpg *.jpeg *.txt *.c *.exe
exclude src/starrail/config/*.json
exclude src/starrail/bin/scheduler/config/*.json
exclude src/starrail/automation/config/automation-data/*.json
================================================
FILE: README.md
================================================
# Honkai: Star Rail - `starrail`
<p align="center">
<img src="https://i.imgur.com/lE9hrlV.png" height="auto" alt="Centered Image"/>
</p>
   
## Overview
The `starrail` package is a CLI (command-line interface) tool designed for managing and interacting with the game, Honkai: Star Rail, directly from your terminal.
### Key Features:
- **Game Launch Control**
- Start/Stop the game application directory from the terminal (skips launcher).
- Schedule the game to start/stop at any given time (i.e. 10:30AM).
- **Simple Automation**
- Custom Macros: Record and playback recorded mouse + keyboard click sequences in-game.
- Uniform Click: Allows automatic uniform-interval mouse clicking (duration can be randomized).
- **Binary Decoding**
- Web Cache: Access decoded web cache URLs containing information about pulls, events, and announcements.
- Streaming Assets: Access detailed client streaming asset information (read-only).
- Supplement Binary Decoder: Provides supplementary tools to decode any ASCII-based binary file.
- **Other Features**
- Screenshots: Open the screenshots directory from the CLI without the game launcher.
- Official Pages: Open the official HoyoVerse web pages easily from the CLI.
<br/>
> [!NOTE]
> **Installation Requirements**
> - Windows 10 or later
> - Python 3.7 or later
> - Official Honkai: Star Rail Installation
<br/>
Please review the CHANGELOG for the latest project updates and the [developer's note](https://github.com/ReZeroE/StarRail/wiki/99.-Developer's-Note) for more information regarding the project overhaul.
<br/>
***
<br/>

# Installation / Setup
**STEP 1 - To Install** the `starrail` package, run with **admin permissions**:
```shell
> pip install starrail==1.0.5
OR
> git clone https://github.com/ReZeroE/StarRail.git
> cd StarRail/
> pip install -e .
```
<br/>
**STEP 2 - To configure** the `starrail` module after installing, run:
```shell
> starrail configure
```
<br/>
**STEP 3 - To verify** that the installation was successful, run:
```shell
> starrail config
```
<br/>
> [!NOTE]
> If you encounter any issues during the installation/setup process, visit [this page](https://github.com/ReZeroE/StarRail/wiki/99.-Common-Setup-Issues) for more help!
<br/>
***
<br/>
# Start CLI
The `starrail` package provides its own standalone CLI environment. You may access the CLI by running:
```shell
> starrail
```
This will bring up the StarRail CLI environment where all the commands can be executed without the `starrail` prefix.

The StarRail CLI environment facilitates efficient execution of the package's supported commands (see Usage Guide section below). Although all commands can be executed directly outside of the CLI environment in any terminal, activating the StarRail CLI allows you to run commands without the `starrail` prefix.
> [!NOTE]
>
> Inside of the StarRail CLI:
> ```shell
> > about
> ```
>
> Outside of the StarRail CLI (any terminal):
> ```shell
> > starrail about
> ```
For this guide, all following commands will be shown as if the CLI environment has not been activated.
<br/>
***
<br/>
# Usage Guide
Below is a brief overview of all the features supported by the `starrail` package. Each section includes a link to a full documentation page where each function is detailed comprehensively.
- [Package Info](https://github.com/ReZeroE/StarRail/wiki/3.-Package-Information)
- About Package
- Version
- Author
- Repository
- [CLI Launcher & Scheduler](https://github.com/ReZeroE/StarRail/wiki/4.-Start-Stop-&-Schedule-Game)
- Start Game
- Stop Game
- Schedule Start/Stop
- [Game Configuration](https://github.com/ReZeroE/StarRail/wiki/5.-Game-Configurations)
- Real-time Game Status
- Base Game Information
- Detailed Client Information
- [Simple Automation](https://github.com/ReZeroE/StarRail/wiki/6.-Simple-Automation)
- Custom Automation
- Uniform Clicks
- [Official Page Access](https://github.com/ReZeroE/StarRail/wiki/7.-Official-Page-Access)
- Official Homepage
- Official Youtube Page
- Official HoyoLab Page
- [Binary Utilities](https://github.com/ReZeroE/StarRail/wiki/8.-Binary-Utilities)
- Decoded Web Cache (events, pulls, announcements)
- Cached Pull History
- Supplementary Binary Decoder
- [Misc Utilities](https://github.com/ReZeroE/StarRail/wiki/9.-Misc-Utilities)
- View Screenshots
- View Game Logs
<br/>
For the entire list of commands available in the `starrail` package, run:
```shell
> starrail help
```
<br/>
## 1. Package Info
To see the general information about the `starrail` package, run:
```shell
> starrail about # Shows all information about the package
OR
> starrail version # Shows HSR and SR-CLI package version
> starrail author # Shows author information
> starrail repo [--open] # Shows repository link
```
<br/>
***
<br/>
## 2. Start/Stop & Schedule Game
To start, stop, or schedule the start or stop of Honkai: Star Rail, the following commands are provided.
### ☆ Start Game
To start Honkai: Star Rail, run:
```shell
> starrail start
```
<br/>
### ☆ Stop Game
To stop Honkai: Star Rail, run:
```shell
> starrail stop
```
<br/>
### ☆ Schedule Start/Stop
To schedule the start/stop of Honkai: Star Rail at a given time, you may use the scheduler supplied in the `starrail` package.
To view the scheduler's help panel, run:
```shell
> starrail schedule
```
**Supported Scheduler Commands** (see usage below)
1. Show Schedule
2. Add Schedule
3. Remove Schedule
4. Clear (remove all) Schedules
```
Example Command Description
---------------------------------------- ----------------------------------------------
starrail schedule add --time 10:30 --action start Schedule Honkai Star Rail to START at 10:30 AM
starrail schedule add --time 15:30 --action stop Schedule Honkai Star Rail to STOP at 3:30 PM
starrail schedule remove Remove an existing scheduled job
starrail schedule show Show all scheduled jobs and their details
starrail schedule clear Cancel all scheduled jobs (irreversible)
```
> [!NOTE]
> **For the full documentation on scheduling**, visit [this page](https://github.com/ReZeroE/StarRail/wiki/4.-CLI-Launcher-&-Scheduler).
<br/>
***
<br/>
## 3. Game Configuration
To view Honkai: Star Rail's configuration as well as its detailed client information, several commands are provided.
### ☆ Real-time Game Status
To show the real-time status of the game while it's running, run:
```shell
> starrail status
```
```
HSR Status Details
------------- -------------------------------------
Status ✓ Running
Process ID 59420
Started On 2024-0x-xx 15:11:04
CPU Percent 1.3%
CPU Affinity 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
IO Operations Writes: 247728, Reads: 580093
```
To show live game status non-stop, run with argument `-l` or `--live`:
```shell
> starrail status [-l|--live]
```
<br/>
### ☆ Base Game Information
To view the game's basic information configured under the `starrail` package listed below, run:
- Game Version
- Game Executable Location
- Game Screenshots Directory
- Game Logs Directory
- Game Executable SHA256
```shell
> starrail config
```
```
Title Details Related Command
------------------ ------------------------------------------------ --------------------
Game Version 2.3.0 starrail version
Game Executable E:\Star Rail\Game\StarRail.exe starrail start/stop
Game Screenshots E:\Star Rail\Game\StarRail_Data\ScreenShots starrail screenshots
Game Logs E:\Star Rail\logs starrail game-logs
Game (.exe) SHA256 2axxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2c
```
<br/>
### ☆ Detailed Client Information
To view detailed information about Honkai: Star Rail, run:
- Client Version
- Client Datetime
- Client Detailed Version
- Application Identifier
- Service Entrypoint(s)
- Client Engine Version
- Other Information
```shell
> starrail details
```
```
Title Details (binary)
---------------------- --------------------------------------------------------
Version V2.3Live
Datetime String 20240607-2202
Detailed Version 7x02406xx-2xx2-V2.3Live-7xxxxx5-CNPRODWin2.3.0-CnLive-v2
Application Identifier com.miHoYo.hkrpg
Service Endpoints https://globaldp-prod-cn0x.bhsr.com/query_dispatch
Engine Version EngineReleaseV2.3
Other StartAsset
StartDesignData
dxxxxxxxxb
f4xxxxxxxxxxxxxxxxxxxxxxa7
```
<br/>
***
<br/>
## 4. Simple Automation
The `starrail` package supports two automation features to assist with the gameplay of Honkai: Star Rail.
### ☆ Custom Automation
This feature enables users to record a sequence of keyboard and mouse actions and replay them within the game. It is specifically optimized for Honkai: Star Rail, ensuring that all actions are only recorded and executed when the game is in focus, thereby preventing unintentional interactions with other applications.
To start, enter the following to bring up the help panel for automation:
```shell
> starrail automation
```
```
Example Command Description
-------------------------- ----------------------------------------------------
starrail automation record Create and record a new automation sequence (macros)
starrail automation show List all recorded automation sequences
starrail automation run Run a recorded automation sequence
starrail automation remove Delete a recorded automation sequence
starrail automation clear Delete all recorded automation sequences
```
> [!NOTE]
> **For a full documentation / guide on custom automation**, visit [this page](https://github.com/ReZeroE/StarRail/wiki/6.-Simple-Automation).
<br/>
### ☆ Uniform Clicks
This feature assists with repetitive mouse clicking (such as for "Start Again" after completing a stage in HSR).
To use this feature, run:
```shell
> starrail click
```
<br/>
**For example**, to control the mouse to click:
1. Once every 5 seconds and
2. Hold for 2 seconds each click
```shell
> starrail click --interval 5 --randomize 1 --hold 2
```
<br/>
***
<br/>
## 5. Official Pages
The `starrail` package supports the following simple commands to access Honkai: Star Rail's official pages.
### ☆ Offical Homepage
To start Honkai: Star Rail's Official Home Page, run:
```shell
> starrail homepage
OR
> starrail homepage -cn # CN Homepage
```
<br/>
### ☆ Official HoyoLab Page
To start Honkai: Star Rail's Official HoyoLab Page, run:
```shell
> starrail hoyolab
```
<br/>
### ☆ Offical Youtube Page
To start Honkai: Star Rail's Official Youtube Page, run:
```shell
> starrail youtube
```
<br/>
### ☆ Offical BiliBili Page (CN)
To start Honkai: Star Rail's Official BiliBili Page, run:
```shell
> starrail bilibili
```
<br/>
***
<br/>
## 6. Binary Utilities
The `starrail` package supports a list of binary-related utility commands for web cache and streaming assets decoding.
### ☆ Web Cache
Honkai: Star Rail's web cache stores recent web data. To access the decoded web cache URLs containing cached information about events, pulls, and announcements without loading into the game, run:
```shell
> starrail webcache
```
The results will be listed in two sections:
1. Events/Pulls Web Cache
2. Announcements Web Cache
<br/>
### ☆ Cached Pulls
To access your Honkai: Star Rail's cached pulls information without logging in to the game, run:
```shell
> starrail pulls
```
Web view of all the pull information will open in the default browser.
<br/>
### ☆ Supplement Binary Decoder
The package provides this supplementary tools to decode any ASCII-based binary file.
```shell
> starrail decode --path <path_to_file>
```
All ASCII-based information will be outputted into a table with index listings as following:
```
Index Content
------- --------------
0 This is a test
1 /Root xxxx 0 R
2 /Info 1 0 R>>
3 start ref
... .....
```
<br/>
***
<br/>
## 7. Misc Utilities
The `starrail` package provides the following quality-of-life utility features to quickly access key game information.
### ☆ Access Screenshots
To access the screenshots without the client or searching through the directory, run:
```shell
> starrail screenshots
```
<br/>
### ☆ Access Game Logs
To access the game's log files, run:
```shell
> starrail game-logs
```
<br/>
### ☆ Session runtime
To get the runtime of the current Honkai: Star Rail session (how long the game has been running since it started), run:
```shell
> starrail runtime
```
<br/>
***
<br/>
## Disclaimer
The "starrail" Python 3 module is an external CLI tool
designed to automate the gameplay of Honkai Star Rail. It is designed
solely interacts with the game through the existing user interface,
and it abides by the Fair Gaming Declaration set forth by COGNOSPHERE
PTE. LTD. The package is designed to provide a streamlined and
efficient way for users to interact with the game through features
already provided within the game, and it does not, in any way, intend
to damage the balance of the game or provide any unfair advantages.
The package does NOT modify any files in any way.
The creator(s) of this package has no relationship with MiHoYo, the
game's developer. The use of this package is entirely at the user's
own risk, and the creator accepts no responsibility for any damage or
loss caused by the package's use. It is the user's responsibility to
ensure that they use the package according to Honkai Star Rail's Fair
Gaming Declaration, and the creator accepts no responsibility for any
consequences resulting from its misuse, including game account
penalties, suspension, or bans.
By using this package, the user agrees to ALL terms and conditions
and acknowledges that the creator will not be held liable for any
negative outcomes that may occur as a result of its use.
<br/>
***
<br/>
## Repository Star History
[](https://star-history.com/#ReZeroE/StarRail&Date)
<br/>
***
<br/>
## License
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/0c/MIT_logo.svg/220px-MIT_logo.svg.png" align="left" width="150"/>
<ul>
- MIT Licensed
</ul>
<br clear="left"/>
================================================
FILE: pyproject.toml
================================================
[build-system]
requires = [
"setuptools>=42",
"tabulate",
"termcolor",
"pytest",
"psutil",
"pynput",
"pyautogui",
"wheel"
]
build-backend = "setuptools.build_meta"
================================================
FILE: requirements.txt
================================================
tabulate
termcolor
pytest
psutil
pynput
pyautogui
pywin32
screeninfo
schedule
pyreadline3
================================================
FILE: setup.cfg
================================================
[metadata]
name = starrail
version = 1.0.5
author = Kevin L.
author_email = kevinliu@vt.edu
description = Honkai: Star Rail Command Line Tool (CLI)
long_description = file: README.md
long_description_content_type = text/markdown
license_files = LICENSE
url = https://github.com/ReZeroE/StarRail
project_urls =
Bug Tracker = https://github.com/ReZeroE/StarRail/issues
classifiers =
Programming Language :: Python :: 3
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Development Status :: 1 - Planning
[options]
package_dir =
= src
packages = find:
python_requires = >=3.7
install_requires = file: requirements.txt
include_package_data = True
license = MIT
[options.packages.find]
packages = starrail
where = src
[options.entry_points]
console_scripts =
starrail = starrail.entrypoints.entrypoints:start_starrail
================================================
FILE: setup.py
================================================
import os
from setuptools import setup, find_packages
setup(
name="starrail",
version="1.0.5",
author="Kevin L.",
author_email="kevinliu@vt.edu",
description="Honkai: Star Rail Command Line Tool (CLI)",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://github.com/ReZeroE/StarRail",
project_urls={
"Bug Tracker": "https://github.com/ReZeroE/StarRail/issues",
},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Development Status :: 1 - Planning",
],
license="MIT",
package_dir={"": "src"},
packages=find_packages(where="src"),
python_requires=">=3.7",
install_requires=open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "requirements.txt")).read().splitlines(),
include_package_data=True,
entry_points={
"console_scripts": [
"starrail = starrail.entrypoints.entrypoints:start_starrail",
],
},
)
================================================
FILE: src/starrail/__init__.py
================================================
"""
StarRail
=====
Honkai: Star Rail CLI Toolkit
"""
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from starrail.constants import AUTHOR, VERSION, COMMAND, GAME_NAME
__author__ = AUTHOR
__version__ = VERSION
__support__ = GAME_NAME
__all__ = [COMMAND]
================================================
FILE: src/starrail/automation/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/automation/config/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/automation/config/automation_config_handler.py
================================================
from starrail.utils.utils import *
from starrail.utils.json_handler import JSONConfigHandler
class StarRailAutomationConfig:
def __init__(self):
self.automation_data_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), f"automation-data")
if not os.path.isdir(self.automation_data_dir):
os.mkdir(self.automation_data_dir)
def load_all_automations(self):
sequence_name_list = os.listdir(self.automation_data_dir)
raw_json_configs = []
for seq_filename in sequence_name_list:
AUTOMATION_FILE = os.path.join(self.automation_data_dir, seq_filename)
config_handler = JSONConfigHandler(AUTOMATION_FILE)
if config_handler.CONFIG_EXISTS():
raw_json_config = config_handler.LOAD_CONFIG() # Loads the sequence config file
raw_json_configs.append(raw_json_config)
return raw_json_configs
def load_automation(self, automation_name):
config_handler = JSONConfigHandler(os.path.join(self.automation_data_dir, f"automation-{automation_name}.json"))
return config_handler.LOAD_CONFIG()
def save_automation(self, automation_name, payload):
config_handler = JSONConfigHandler(os.path.join(self.automation_data_dir, f"automation-{automation_name}.json"))
return config_handler.SAVE_CONFIG(payload)
def delete_automation(self, automation_name):
config_handler = JSONConfigHandler(os.path.join(self.automation_data_dir, f"automation-{automation_name}.json"))
if config_handler.CONFIG_EXISTS():
return config_handler.DELETE_CONFIG()
return False
================================================
FILE: src/starrail/automation/pixel_calculator/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/automation/pixel_calculator/pixel_calculator.py
================================================
from starrail.utils.utils import aprint, LogType
from starrail.automation.pixel_calculator.resolution_detector import ResolutionDetector
class PixelCalculator:
def __init__(self, monitor_info: dict):
self.prev_monitor_width = monitor_info["width"]
self.prev_monitor_height = monitor_info["height"]
current_monitor_info = ResolutionDetector.get_primary_monitor_size()
self.current_monitor_width = current_monitor_info["width"]
self.current_monitor_height = current_monitor_info["height"]
@staticmethod
def transform_coordinate(prev_coor: tuple, prev_window_info: dict):
try:
x, y = prev_coor
curr_window_info = ResolutionDetector.get_foreground_window_size()
curr_x = curr_window_info["left"]
curr_y = curr_window_info["top"]
curr_w = curr_window_info["width"]
curr_h = curr_window_info["height"]
# print(curr_x, curr_y, curr_w, curr_h)
prev_x = prev_window_info["left"]
prev_y = prev_window_info["top"]
prev_w = prev_window_info["width"]
prev_h = prev_window_info["height"]
# Normalize the coordinate relative to the original window
normalized_x = (x - prev_x) / prev_w
normalized_y = (y - prev_y) / prev_h
new_x = curr_x + normalized_x * curr_w
new_y = curr_y + normalized_y * curr_h
return (int(new_x), int(new_y))
except KeyError:
# Unavailable for pixel calculator (ver 0.0.2)
return prev_coor
================================================
FILE: src/starrail/automation/pixel_calculator/resolution_detector.py
================================================
import time
import win32process
import pygetwindow
from ctypes import windll
from screeninfo import get_monitors
from win32gui import GetWindowRect, GetForegroundWindow
from starrail.exceptions.exceptions import *
from starrail.utils.process_handler import ProcessHandler
class ResolutionDetector:
windll.user32.SetProcessDPIAware()
@staticmethod
def get_primary_monitor_size() -> dict:
monitors = get_monitors()
for m in monitors:
if m.is_primary:
monitor_info = {
"width": m.width,
"height": m.height
}
return monitor_info
return None
@staticmethod
def get_foreground_window_size():
window_size = GetWindowRect(GetForegroundWindow())
monitor_size = ResolutionDetector.get_primary_monitor_size()
return {
'left' : window_size[0],
'top' : window_size[1],
'width' : window_size[2],
'height' : window_size[3],
'is_fullscreen' : window_size[2] == monitor_size["width"] and window_size[2] == monitor_size["height"]
}
@staticmethod
def get_window_size():
pid = ProcessHandler.get_focused_pid()
# print(f"Currently focused PID: {pid}")
if pid == None: return None
win_info: dict = ResolutionDetector.get_window_info(pid)
if win_info == None:
raise Exception(f"Failed to fetch window size. No results returned (PID {pid}).")
monitor_info = ResolutionDetector.get_primary_monitor_size()
if monitor_info["width"] == win_info["width"] and \
monitor_info["height"] == win_info["height"] and \
win_info["top"] == 0 and win_info["left"] == 0:
win_info["is_fullscreen"] = True
else:
win_info["is_fullscreen"] = False
return win_info
@staticmethod
def get_window_info(pid, retry: int = 5):
for _ in range(retry):
windows = pygetwindow.getWindowsWithTitle('') # Get all windows
for window in windows:
_, window_pid = win32process.GetWindowThreadProcessId(window._hWnd)
if window_pid == pid:
if (window.left >= 0 and window.top >= 0) and (window.width > 0 and window.height > 0):
return {
'left' : window.left,
'top' : window.top,
'width' : window.width,
'height' : window.height,
'is_fullscreen' : None
}
time.sleep(1)
return None
================================================
FILE: src/starrail/automation/recorder.py
================================================
import time
import threading
import tkinter as tk
import asyncio
from pynput import mouse, keyboard
from starrail.constants import RECORDER_WINDOW_INFO, CALIBRATION_MONITOR_INFO
from starrail.automation.units.action import Action, MouseAction, KeyboardAction, ScrollAction
from starrail.automation.units.sequence import AutomationSequence
from starrail.automation.pixel_calculator.resolution_detector import ResolutionDetector
from starrail.utils.utils import *
from starrail.exceptions.exceptions import *
from starrail.controllers.star_rail_app import HonkaiStarRail
'''
MouseAction
1. Action delay
ScrollAction
1. Action delay
KeyboardAction:
1. Action delay
2. Hold time (0.9.0)
'''
class AutomationRecorder():
def __init__(self, sequence: AutomationSequence, starrail_instance: HonkaiStarRail):
self.sequence = sequence
self.starrail = starrail_instance
self.label = None
self.prev_action_time = None
self.is_recording = False
self.stop_event = threading.Event()
self.keyboard_key = None
self.keyboard_press_time = None
def create_indicator_window(self):
LENGTH, WIDTH, RADIUS = self.__scale_window(RECORDER_WINDOW_INFO['height'], RECORDER_WINDOW_INFO['width'], RECORDER_WINDOW_INFO["border-radius"])
root = tk.Tk()
root.title("Waiting for game to be focused...")
root.geometry(f"{WIDTH}x{LENGTH}+0+0")
root.overrideredirect(True) # Remove window decorations
root.attributes('-topmost', True) # STAY ON TOOOOOPPPPPP
# Set the overall background color to black and then make it transparent
background_color = 'black'
root.configure(bg=background_color)
root.attributes('-transparentcolor', background_color)
canvas = tk.Canvas(root, bg=background_color, highlightthickness=0)
canvas.pack(fill=tk.BOTH, expand=True)
# Replace 'black' with the color of your choice for the rounded rectangle
canvas_color = '#333333'
canvas.create_polygon(
[
RADIUS, 0, WIDTH - RADIUS, 0,
WIDTH, RADIUS, WIDTH, LENGTH - RADIUS,
WIDTH - RADIUS, LENGTH, RADIUS, LENGTH,
0, LENGTH - RADIUS, 0, RADIUS
],
smooth=True, fill=canvas_color)
self.label = tk.Label(canvas, text="Waiting for game to be focused...", font=('Helvetica', 9), fg='#FFFFFF', bg=canvas_color)
self.label.place(relx=0.4, rely=0.5, anchor='center')
button_width = 120
button_height = 70
button_width, button_height, _ = self.__scale_window(button_width, button_height)
stop_button = tk.Button(canvas, text='Stop\nRecording', font=('Helvetica', 10), command=lambda: self.stop_recording(root), bg='#14628c', fg='#FFFFFF')
stop_button.place(relx=0.85, rely=0.5, anchor='center', width=button_width, height=button_height)
# Mouse movement handling
def on_press(event):
root._drag_start_x = event.x
root._drag_start_y = event.y
def on_drag(event):
dx = event.x - root._drag_start_x
dy = event.y - root._drag_start_y
x = root.winfo_x() + dx
y = root.winfo_y() + dy
root.geometry(f"+{x}+{y}")
root.bind('<Button-1>', on_press)
root.bind('<B1-Motion>', on_drag)
self.label.config(text=f"Waiting for game to be focused...")
root.mainloop()
# =============================================
# ===============| BASE DRIVER | ==============
# =============================================
def stop_recording(self, root: tk.Tk):
self.stop_event.set()
self.is_recording = False
self.sequence.actions = self.sequence.actions[:-1] # Remove the last key click (user clicks on the Stop Recording button)
root.quit()
def pause_or_resume_recording(self):
self.is_recording = not self.is_recording
def record(self, start_on_callback=False) -> AutomationSequence:
threading.Thread(target=self.create_indicator_window, daemon=True).start()
if start_on_callback:
self.is_recording = True
self.prev_action_time = None
aprint(f"Ready to record.\n{Printer.to_lightblue(' - To start')}: Focus onto the game and the recording will automatically start.\n{Printer.to_lightblue(' - To stop')}: Click 'Stop Recording' on the top-left corner of the screen.")
else:
raise Exception("Record must start on callback. Other case not implemented.")
# mouse_listener = mouse.Listener(on_click=self.__on_mouse_action)
# keyboard_listener = keyboard.Listener(on_press=self.__on_keyboard_action)
with mouse.Listener(on_click=self.__on_mouse_action, on_scroll=self.__on_scroll_action) as mouse_listener, \
keyboard.Listener(on_press=self.__on_keyboard_action_press, on_release=self.__on_keyboard_action_release) as keyboard_listener:
mouse_listener_thread = threading.Thread(target=mouse_listener.join)
keyboard_listener_thread = threading.Thread(target=keyboard_listener.join)
mouse_listener_thread.start()
keyboard_listener_thread.start()
# Wait until stop recording event is triggered
self.stop_event.wait()
# Stop listeners
mouse_listener.stop()
keyboard_listener.stop()
# Wait for listeners to finish
mouse_listener_thread.join()
keyboard_listener_thread.join()
time.sleep(0.3)
return self.sequence
# =============================================
# ============| ON-ACTION DRIVER | ============
# =============================================
def __on_mouse_action(self, x, y, button, pressed):
'''
On mouse action is executed twice:
1. when the mouse is pressed
2. when the mouse is released
The application is only focused on-release, therefore we need to ignore the on-press action.
As of version 0.9.0, hold-time has not been implemented.
'''
if pressed or not self.starrail.is_focused():
return
if self.is_recording:
now = time.time()
delay = now - self.prev_action_time if self.prev_action_time else float(0)
clicked = button == mouse.Button.left # If left is clicked, the click is registered as "clicked", else if right is clicked, only the mouse movement will be registered.
self.sequence.add(MouseAction((x, y), delay, clicked, ResolutionDetector.get_foreground_window_size()))
self.prev_action_time = now
self.on_mouse_action_update_window(x, y, delay)
def __on_scroll_action(self, x, y, dx, dy):
if not self.starrail.is_focused():
return
if self.is_recording:
now = time.time()
delay = now - self.prev_action_time if self.prev_action_time else float(0)
self.sequence.add(ScrollAction((x, y), dx, dy, delay, ResolutionDetector.get_foreground_window_size()))
self.prev_action_time = now
self.on_scroll_action_update_window(x, y, dx, dy, delay)
'''
Key hold_time has been implemented as of 0.9.0 and there are a few key notes.
1. Only one key will be recorded at a time. No key or key-mouse combination is supported.
2. When a new key is pressed with the previous key not yet released, the new key will be completely ignored until the release of the previous key.
'''
def __on_keyboard_action_press(self, key):
# NOTE: This function will be executed repeatedly during a key-hold by the keyboard listener (aka param key will remain the same on every iteration).
# If the current key is the same as the previous key, continue.
if key == self.keyboard_key:
pass
# If there is already an press-regeistered key but it's not the current key (a new key is pressed while the
# previous key has not been released), do not register this new key.
elif self.keyboard_key != None and key != self.keyboard_key:
pass
# New key pressed.
else:
self.keyboard_key = key
self.keyboard_press_time = time.time()
def __on_keyboard_action_release(self, key):
# If key released is not the press-registered key, then ignore this key
if key != self.keyboard_key:
return
# If the app is unfocused, reset the press-registered key and time
if not self.starrail.is_focused():
self.keyboard_key = None
self.keyboard_press_time = None
return
# Pause/resume listener on Enter key press
elif key == keyboard.Key.space:
self.keyboard_key = None
self.keyboard_press_time = None
self.pause_or_resume_recording()
return # Return to not record this space key press
# Record only if is_recording is set to True
if self.is_recording:
now = time.time()
hold_time = now - self.keyboard_press_time
delay = now - self.prev_action_time - hold_time if self.prev_action_time else float(0)
self.sequence.add(KeyboardAction(key, delay, hold_time))
self.prev_action_time = now
self.keyboard_key = None
self.keyboard_press_time = None
self.on_keyboard_action_update_window(key, delay, hold_time)
# =============================================
# ============| UPDATE UI WINDOW | ============
# =============================================
def on_mouse_action_update_window(self, x, y, delay):
if self.label:
self.__update_label(text=f"Mouse Click Detected - ({x}, {y})\nDelay: {round(delay, 2)}")
def on_scroll_action_update_window(self, x, y, dx, dy, delay):
if self.label:
if dy > 0:
self.__update_label(text=f"Scroll Detected - ({x}, {y}) - Scrolled UP\nDelay: {round(delay, 2)}")
elif dy < 0:
self.__update_label(text=f"Scroll Detected - ({x}, {y}) - Scrolled DOWN\nDelay: {round(delay, 2)}")
else:
self.__update_label(text="Scroll Detected - Horizontal scroll not supported.")
def on_keyboard_action_update_window(self, key, delay, hold_time):
if self.label:
try:
self.__update_label(text=f"Key Detected - '{key.char}'\nDelay: {round(delay, 2)}, Hold: {round(hold_time, 2)}")
except AttributeError:
self.__update_label(text=f"Key Detected - '{key}'\nDelay: {round(delay, 2)}, Hold: {round(hold_time, 2)}")
def on_pause_action_update_window(self):
if self.is_recording:
self.__update_label(text=f"Recording resumed...")
else:
self.__update_label(text=f"Recording paused...")
def __update_label(self, text: str):
self.label.place(relx=0.4, rely=0.6, anchor='center')
self.label.config(text=f"Total Actions Recorded: {len(self.sequence.actions)}\n{text}\n")
def __scale_window(self, standard_width, standard_height, standard_radius=None):
monitor_info = ResolutionDetector.get_primary_monitor_size()
width_ratio = monitor_info["width"] / CALIBRATION_MONITOR_INFO["width"]
height_ratio = monitor_info["height"] / CALIBRATION_MONITOR_INFO["height"]
if standard_radius != None:
radius = standard_radius * ((width_ratio+height_ratio)/2)
else:
radius = 0
return int(standard_width*width_ratio), int(standard_height*height_ratio), int(radius)
================================================
FILE: src/starrail/automation/units/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/automation/units/action.py
================================================
import re
import time
import pyautogui
import pynput
from pynput import keyboard
from pynput.keyboard import Key
from abc import ABC, abstractclassmethod
from starrail.constants import PYNPUT_KEY_MAPPING
'''
Action: A single mouse of keyboard action.
Sequence: A list/sequence of Action objects.
'''
class Action(ABC):
@abstractclassmethod
def __init__(self, *args):
self.delay = ...
from pynput.keyboard import Key
@abstractclassmethod
def __repr__(self):
pass
@abstractclassmethod
def execute(self, *args):
pass
@abstractclassmethod
def to_json(self):
pass
class MouseAction(Action):
def __init__(self, coor: tuple, delay: float, click: bool, window_info: dict):
self.coordinate = coor
self.delay = delay
self.click = click
self.window_info = window_info
self.is_valid_for_pixel_calc = self.__is_valid_for_pixel_calc()
def execute(self):
'''
The delay represent the time lag between the current click and the previous click,
therefore time.sleep() is executed at the start of a new action.
'''
x = self.coordinate[0]
y = self.coordinate[1]
pyautogui.moveTo(x, y, duration=0.1)
if self.click:
# pyautogui.click(interval=0.1)
pyautogui.mouseDown()
time.sleep(0.1)
pyautogui.mouseUp()
def to_json(self):
return {
"coordinate": {
"x": self.coordinate[0],
"y": self.coordinate[1]
},
"delay": self.delay,
"click": self.click,
"window_info": self.window_info
}
def __repr__(self):
return f"MouseAction(coor={self.coordinate}, delay={round(self.delay, 2)}, click={self.click})"
def __is_valid_for_pixel_calc(self):
try:
assert("width" in self.window_info)
assert("height" in self.window_info)
assert("top" in self.window_info)
assert("left" in self.window_info)
assert("is_fullscreen" in self.window_info)
return True
except AssertionError:
return False
class ScrollAction(Action):
def __init__(self, coor: tuple, dx: float, dy: float, delay: float, window_info: dict):
self.coordinate = coor
self.delay = delay
self.dx = dx
self.dy = dy
self.window_info = window_info
self.is_valid_for_pixel_calc = self.__is_valid_for_pixel_calc()
def execute(self):
'''
Only dy scrolling is supported as of version 0.9
'''
x = self.coordinate[0]
y = self.coordinate[1]
pyautogui.moveTo(x, y, duration=0.1)
time.sleep(0.05)
pyautogui.scroll(self.dy)
def to_json(self):
return {
"coordinate": {
"x": self.coordinate[0],
"y": self.coordinate[1]
},
"scroll": {
"dx": self.dx,
"dy": self.dy
},
"delay": self.delay,
"window_info": self.window_info
}
def __repr__(self):
return f"ScrollAction(coor={self.coordinate}, scroll=(dx={self.dx}, dy={self.dy}) delay={round(self.delay, 2)}"
def __is_valid_for_pixel_calc(self):
try:
assert("width" in self.window_info)
assert("height" in self.window_info)
assert("top" in self.window_info)
assert("left" in self.window_info)
assert("is_fullscreen" in self.window_info)
return True
except AssertionError:
return False
class KeyboardAction(Action):
def __init__(self, key: str, delay: float, hold_time: float):
self.key = self.reformat_key(key)
self.delay = delay
self.hold_time = hold_time if hold_time > 0.05 else 0.05 # If hold time < 0.05, then use 0.05 instead (or else the key might not register)
def execute(self, keyboard):
'''
The delay represent the time lag between the current click and the previous click,
therefore time.sleep() is executed at the start of a new action.
'''
self.press_key(self.key, keyboard)
def to_json(self):
return {
"key": self.key,
"delay": self.delay,
"hold_time": self.hold_time
}
def __repr__(self):
return f"KeyboardAction(key={self.key}, delay={round(self.delay, 2)}, hold_time={round(self.hold_time, 2)})"
def press_key(self, key: str, pynput_keyboard: pynput.keyboard.Controller):
'''
This is going to be a little difficult to explain, but essentially the string format keys
recorded by pynput can't be re-recognized by pynput for execution. Therefore, a mapping
between the string formated keys (collected by pynput itself) and the actual Key object
is used to convert the key when trying to execute the keyboard action.
There are going to be a wide range of keys that aren't supported including "hotkeys" or
a combination of two or more different keys. This will need to be specified in the
ongoing documentation.
'''
# TODO: find a better way to implement this, if possible
pynput_key = PYNPUT_KEY_MAPPING.get(key, key)
try:
pynput_keyboard.press(pynput_key)
time.sleep(self.hold_time)
pynput_keyboard.release(pynput_key)
except (keyboard.Controller.InvalidKeyException, ValueError) as ex:
# TODO: log this unsupported key as opposed to print it
# print(f"Unsupported key: <{key}>")
pass
def reformat_key(self, key: keyboard.Key) -> str:
key: str = str(key).strip().replace("'", "")
removing = ["_r", "_gr", "_l"]
for suffix in removing:
if key.endswith(suffix):
key = key.replace(suffix, "")
return key
================================================
FILE: src/starrail/automation/units/sequence.py
================================================
import os
import time
import copy
from datetime import datetime
from pynput import keyboard
from starrail.automation.units.action import Action, MouseAction, ScrollAction, KeyboardAction
from starrail.exceptions.exceptions import SRExit
from starrail.constants import VERSION
from starrail.utils.utils import *
from starrail.automation.pixel_calculator.resolution_detector import ResolutionDetector
from starrail.automation.pixel_calculator.pixel_calculator import PixelCalculator
SUBMODULE_NAME = "AUTO"
class AutomationSequence:
def __init__(
self,
sequence_name: str
):
self.sequence_name = sequence_name # Default to None (populated during some __init__() and parse_json())
self.date_created: datetime = None # Default to None (and set at to_json() and parse_json())
self.primary_monitor_info: dict = None # Fetch PRIMARY monitor's size (width x height)
self.other_data = None
self.actions: list[Action] = []
self.global_delay = 0
def __progress_bar(self, actions: list, prefix="", size=40, out=sys.stdout):
count = len(actions)
start = time.time() # time estimate start
total_time = self.get_runtime()
def show(j, delay, remaining_time_str):
x = int(size*j/count)
aprint(f"{prefix}|{u'█'*x}{(' '*(size-x))}| {int(j)}/{count} - Remaining: {remaining_time_str}", end='\r', file=out, flush=True)
def secs_to_str(secs):
mins, sec = divmod(secs, 60)
time_str = f"{int(mins)} mins {round(sec, 2)} secs"
return time_str
show(0.1, delay=actions[0].delay, remaining_time_str=secs_to_str(total_time)) # avoid div/0
for i, action in enumerate(actions):
yield action
total_time -= action.delay
show(i+1, action.delay, secs_to_str(total_time))
print("", flush=True, file=out)
# def normalize(self):
# '''
# Introduced in version 0.9.0, the automation will be self-normalized to merge hold actions.
# '''
# NORMALIZE_THRESHOLD = 0.05
# actions_copy = copy.deepcopy(self.actions)
# prev_action = None
# for idx, action in enumerate(self.actions):
# if isinstance(action, KeyboardAction) and isinstance(prev_action, KeyboardAction):
# if action["delay"]
def execute(self):
def verbose_action(idx: int, action: Action):
buffer_space = " "*5 # Verbose current action
# aprint(f"(CMD {idx+1}/{len(self.actions)}) Executing: {action.__repr__()}{buffer_space}", end="\r")
aprint(f"[Action {idx}/{len(self.actions)}] Running... ", submodule_name=SUBMODULE_NAME, end="\r")
def verbose_warning():
# Verbose warning if the current action isn't suited for the pixel calculator developed in ver0.0.2+
for action in self.actions:
if isinstance(action, MouseAction) and action.is_valid_for_pixel_calc == False:
aprint(f"This automation sequence is not available for pixel calculator in version {VERSION}.")
return
# Becuase the pixel calculator will directory modify the MouseAction's coordinates, we need a way to reset the
# sequence's coordinates after the sequence finishes running. Therefore, we first make a copy of the sequence
# before it is modified by the pixel calculator and then replace the modified sequence at the end.
actions_copy = copy.deepcopy(self.actions)
verbose_warning()
pynput_keyboard = keyboard.Controller()
# for idx, action in enumerate(self.__progress_bar(self.actions, f"Running: ", 40)):
for idx, action in enumerate(self.actions):
verbose_action(idx, action)
time.sleep(action.delay) # Execute current action after standard action delay
time.sleep(self.global_delay) # Execute current action after global delay
# ====================================
# ===========| KEYBOARD | ============
# ====================================
if isinstance(action, KeyboardAction):
action.execute(pynput_keyboard)
# ==================================
# ============| MOUSE | ============
# ==================================
elif isinstance(action, MouseAction):
if action.is_valid_for_pixel_calc == True:
try:
new_coord = PixelCalculator.transform_coordinate(action.coordinate, action.window_info)
action.coordinate = new_coord
except Exception as ex:
# TODO: log this error
# aprint(Printer.to_lightgrey(f"Warning: PixelCalc not working ({ex})"))
pass
action.execute()
# ==================================
# ===========| SCROLL | ============
# ==================================
elif isinstance(action, ScrollAction):
action.execute()
# Reset the modified version of the sequence (modified by the pixel calculator)
self.actions = actions_copy
def add(self, action: Action):
assert(isinstance(action, Action))
self.actions.append(action)
def to_json(self):
json_data = dict()
json_data["metadata"] = {
"sequence_name" : self.sequence_name,
"date_created" : DatetimeHandler.datetime_to_str(self.date_created),
"monitor_info" : ResolutionDetector.get_primary_monitor_size(),
"other_data" : self.other_data
}
json_data["actions_sequence"] = [action.to_json() for action in self.actions]
return json_data
@staticmethod
def parse_config(raw_json_config: list):
metadata = raw_json_config["metadata"]
sequence_name = metadata["sequence_name"]
sequence = AutomationSequence(sequence_name)
sequence.date_created = DatetimeHandler.str_to_datetime(metadata["date_created"])
sequence.primary_monitor_info = metadata["monitor_info"]
sequence.other_data = metadata["other_data"]
actions_sequence = raw_json_config["actions_sequence"]
for raw_action in actions_sequence:
# MOUSE ACTION ================================================
if "click" in raw_action:
mouse_action = MouseAction(
(
raw_action["coordinate"]["x"],
raw_action["coordinate"]["y"]
),
raw_action["delay"],
raw_action["click"],
raw_action["window_info"]
)
sequence.actions.append(mouse_action)
# SCROLL ACTION ================================================
elif "scroll" in raw_action:
scroll_action = ScrollAction(
(
raw_action["coordinate"]["x"],
raw_action["coordinate"]["y"]
),
raw_action["scroll"]["dx"],
raw_action["scroll"]["dy"],
raw_action["delay"],
raw_action["window_info"]
)
sequence.actions.append(scroll_action)
# KEYBOARD ACTION ==============================================
elif "key" in raw_action:
keyboard_action = KeyboardAction(
raw_action["key"],
raw_action["delay"],
raw_action["hold_time"]
)
sequence.actions.append(keyboard_action)
else:
raise Exception(f"Action ({raw_action}) can't be interpreted!")
return sequence
def print_sequence(self):
for action in self.actions:
print(action)
def set_date_created_to_current(self):
self.date_created = DatetimeHandler.get_datetime()
def set_global_delay(self, global_delay: int):
self.global_delay = global_delay
def get_runtime(self):
runtime = 0
for action in self.actions:
runtime += action.delay
runtime += self.global_delay
return round(runtime, 2)
# New feature in 1.0.0 designed to auto-correct consecutive key holding
# def auto_correct(self):
# actions_copy = copy.deepcopy(self.actions)
# for idx, action in enumerate(self.actions):
# if isinstance(action, KeyboardAction):
# if actions_copy[idx].key == actions_copy[idx+1].key:
# actions_copy[idx].delay += actions_copy[idx+1].delay
================================================
FILE: src/starrail/bin/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/bin/loader/__init__.py
================================================
================================================
FILE: src/starrail/bin/loader/loader.py
================================================
from itertools import cycle
from shutil import get_terminal_size
from threading import Thread
from time import sleep
class Loader:
def __init__(self, desc="Loading...", end="Done!", timeout=0.1):
"""
A loader-like context manager
Args:
desc (str, optional): The loader's description. Defaults to "Loading...".
end (str, optional): Final print. Defaults to "Done!".
timeout (float, optional): Sleep time between prints. Defaults to 0.1.
"""
self.desc = desc
self.end = end
self.timeout = timeout
self._thread = Thread(target=self._animate, daemon=True)
self.steps = ['|', '/', '-', '\\']
self.done = False
def start(self):
self._thread.start()
return self
def _animate(self):
for c in cycle(self.steps):
if self.done:
break
print(f"\r{self.desc} {c}", flush=True, end="")
sleep(self.timeout)
def __enter__(self):
self.start()
def stop(self):
self.done = True
cols = get_terminal_size((80, 20)).columns
# print("\r" + " " * cols, end="", flush=True)
if self.end != None:
print(f"\r{self.end}", flush=True)
def __exit__(self, exc_type, exc_value, tb):
# handle exceptions with those variables ^
self.stop()
if __name__ == "__main__":
with Loader("Loading with context manager..."):
for i in range(10):
sleep(0.25)
loader = Loader("Loading with object...", "That was fast!", 0.05).start()
# for i in range(10):
sleep(5)
loader.stop()
================================================
FILE: src/starrail/bin/logs/starrail_log.txt
================================================
================================================
FILE: src/starrail/bin/pick/__init__.py
================================================
# The MIT License (MIT)
# Copyright (c) 2016 Wang Dàpéng
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/bin/pick/pick.py
================================================
# The MIT License (MIT)
# Copyright (c) 2016 Wang Dàpéng
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import curses
from colorama import init as colorama_init; colorama_init()
from dataclasses import dataclass, field
from typing import Any, List, Optional, Sequence, Tuple, TypeVar, Union, Generic
__all__ = ["Picker", "pick", "Option"]
@dataclass
class Option:
label: str
value: Any
KEYS_ENTER = (curses.KEY_ENTER, ord("\n"), ord("\r"))
KEYS_UP = (curses.KEY_UP, ord("w"))
KEYS_DOWN = (curses.KEY_DOWN, ord("s"))
KEYS_SELECT = (curses.KEY_RIGHT, ord(" "))
SYMBOL_CIRCLE_FILLED = "[x]"
SYMBOL_CIRCLE_EMPTY = "[ ]"
OPTION_T = TypeVar("OPTION_T", str, Option)
PICK_RETURN_T = Tuple[OPTION_T, int]
@dataclass
class Picker(Generic[OPTION_T]):
options: Sequence[OPTION_T]
title: Optional[str] = None
indicator: str = ">"
default_index: int = 0
multiselect: bool = False
min_selection_count: int = 0
selected_indexes: List[int] = field(init=False, default_factory=list)
index: int = field(init=False, default=0)
screen: Optional["curses._CursesWindow"] = None
def __post_init__(self) -> None:
if len(self.options) == 0:
raise ValueError("options should not be an empty list")
if self.default_index >= len(self.options):
raise ValueError("default_index should be less than the length of options")
if self.multiselect and self.min_selection_count > len(self.options):
raise ValueError(
"min_selection_count is bigger than the available options, you will not be able to make any selection"
)
self.index = self.default_index
def move_up(self) -> None:
self.index -= 1
if self.index < 0:
self.index = len(self.options) - 1
def move_down(self) -> None:
self.index += 1
if self.index >= len(self.options):
self.index = 0
def mark_index(self) -> None:
if self.multiselect:
if self.index in self.selected_indexes:
self.selected_indexes.remove(self.index)
else:
self.selected_indexes.append(self.index)
def get_selected(self) -> Union[List[PICK_RETURN_T], PICK_RETURN_T]:
"""return the current selected option as a tuple: (option, index)
or as a list of tuples (in case multiselect==True)
"""
if self.multiselect:
return_tuples = []
for selected in self.selected_indexes:
return_tuples.append((self.options[selected], selected))
return return_tuples
else:
return self.options[self.index], self.index
def get_title_lines(self) -> List[str]:
if self.title:
return self.title.split("\n") + [""]
return []
def get_option_lines(self) -> List[str]:
lines: List[str] = []
for index, option in enumerate(self.options):
if not self.multiselect:
if index == self.index:
prefix = SYMBOL_CIRCLE_FILLED
else:
prefix = SYMBOL_CIRCLE_EMPTY
else:
if index == self.index:
prefix = self.indicator
else:
prefix = len(self.indicator) * " "
if self.multiselect:
symbol = (
SYMBOL_CIRCLE_FILLED
if index in self.selected_indexes
else SYMBOL_CIRCLE_EMPTY
)
prefix = f"{prefix} {symbol}"
option_as_str = option.label if isinstance(option, Option) else option
lines.append(f"{prefix} {option_as_str}")
return lines
def get_lines(self) -> Tuple[List, int]:
title_lines = self.get_title_lines()
option_lines = self.get_option_lines()
lines = title_lines + option_lines
current_line = self.index + len(title_lines) + 1
return lines, current_line
def draw(self, screen: "curses._CursesWindow") -> None:
"""draw the curses ui on the screen, handle scroll if needed"""
screen.clear()
x, y = 1, 1 # start point
max_y, max_x = screen.getmaxyx()
max_rows = max_y - y # the max rows we can draw
lines, current_line = self.get_lines()
# calculate how many lines we should scroll, relative to the top
scroll_top = 0
if current_line > max_rows:
scroll_top = current_line - max_rows
lines_to_draw = lines[scroll_top : scroll_top + max_rows]
for line in lines_to_draw:
screen.addnstr(y, x, line, max_x - 2)
y += 1
screen.refresh()
def run_loop(
self, screen: "curses._CursesWindow"
) -> Union[List[PICK_RETURN_T], PICK_RETURN_T]:
while True:
self.draw(screen)
c = screen.getch()
if c in KEYS_UP:
self.move_up()
elif c in KEYS_DOWN:
self.move_down()
elif c in KEYS_ENTER:
if (
self.multiselect
and len(self.selected_indexes) < self.min_selection_count
):
continue
return self.get_selected()
elif c in KEYS_SELECT and self.multiselect:
self.mark_index()
def config_curses(self) -> None:
try:
# use the default colors of the terminal
curses.use_default_colors()
# hide the cursor
curses.curs_set(0)
except:
# Curses failed to initialize color support, eg. when TERM=vt100
curses.initscr()
def _start(self, screen: "curses._CursesWindow"):
self.config_curses()
return self.run_loop(screen)
def start(self):
if self.screen:
# Given an existing screen
# don't make any lasting changes
last_cur = curses.curs_set(0)
ret = self.run_loop(self.screen)
if last_cur:
curses.curs_set(last_cur)
return ret
return curses.wrapper(self._start)
def pick(
options: Sequence[OPTION_T],
title: Optional[str] = None,
indicator: str = ">",
default_index: int = 0,
multiselect: bool = False,
min_selection_count: int = 0,
screen: Optional["curses._CursesWindow"] = None,
):
picker: Picker = Picker(
options,
title,
indicator,
default_index,
multiselect,
min_selection_count,
screen,
)
return picker.start()
pick(['a', 'b'])
================================================
FILE: src/starrail/bin/pid/get_active_pid.c
================================================
// Compile:
// gcc .\get_active_pid.c -o ../get_active_pid
#include <windows.h>
#include <stdio.h>
int main() {
DWORD pid;
HWND hwnd = GetForegroundWindow(); // get handle of currently active window
if (hwnd == NULL) {
printf("No active window\n");
return 1;
}
GetWindowThreadProcessId(hwnd, &pid); // get PID
// printf("Active window PID: %lu\n", pid);
printf("%lu\n", pid);
return 0;
}
================================================
FILE: src/starrail/bin/scheduler/__init__.py
================================================
================================================
FILE: src/starrail/bin/scheduler/config/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/bin/scheduler/config/starrail_schedule_config.py
================================================
from starrail.utils.utils import *
from starrail.utils.json_handler import JSONConfigHandler
class StarRailScheduleConfig(JSONConfigHandler):
def __init__(self):
__scheduler_config = os.path.join(os.path.abspath(os.path.dirname(__file__)), "schedules.json")
super().__init__(__scheduler_config, list)
def load_schedule(self):
return self.LOAD_CONFIG()
def save_schedule(self, payload):
ret = self.SAVE_CONFIG(payload)
================================================
FILE: src/starrail/bin/scheduler/starrail_scheduler.py
================================================
import schedule
import time
import threading
from enum import Enum
import re
import tabulate
from starrail.utils.utils import *
from starrail.bin.scheduler.config.starrail_schedule_config import StarRailScheduleConfig
from starrail.controllers.star_rail_app import HonkaiStarRail
SUBMODULE_NAME = "SR-SCL"
SUBMODULE_VERSION = "1.0"
class OperationTypes(Enum):
START = "Start Game"
END = "Stop Game"
class StartRailJob:
def __init__(self, job_id: int, job: schedule.Job, op_type: OperationTypes):
self.job_id = job_id
self.op_type = op_type
self.schedule_job = job
self.interval = job.interval
self.unit = job.unit
self.last_run = job.last_run
self.next_run = job.next_run
def __str__(self):
return f"[{Printer.to_lightpurple('JOB')}] {self.op_type} Game - Next run: {self.next_run}, Last run: {self.last_run}"
def print_job(self):
print(self.__str__())
def to_dict(self):
return {
"id" : self.job_id,
"op_type" : self.op_type.value,
'interval' : self.interval,
'unit' : self.unit,
'next_run' : str(self.next_run),
'last_run' : str(self.last_run),
}
class StarRailScheduler:
def __init__(self, starrail_instance: HonkaiStarRail):
self.starrail = starrail_instance
self.schedule_config = StarRailScheduleConfig()
self.jobs: dict[int, StartRailJob] = dict()
self.__load_schedules() # Load schedule into jobs
self._stop_event = threading.Event()
self.scheduler_thread = threading.Thread(target=self.__run_scheduler, daemon=True)
self.scheduler_thread.start()
# =============================================
# =========| START/STOP FUNCTIONS | ===========
# =============================================
def __load_schedules(self):
schedules: list[StartRailJob] = self.schedule_config.load_schedule()
if schedules == None:
return
for data in schedules:
job_id = data["id"]
op_type = OperationTypes(data['op_type'])
matched_time = re.search("[0-9]{2}:[0-9]{2}:[0-9]{2}", data['next_run'])
if matched_time:
schedule_time = matched_time.group(0)
else:
aprint(f"Failed to read schedule with data: {data}", log_type=LogType.ERROR)
continue
if op_type == OperationTypes.START:
job = schedule.every(data['interval']).day.at(schedule_time).do(self.starrail.start)
elif op_type == OperationTypes.END:
job = schedule.every(data['interval']).day.at(schedule_time).do(self.starrail.terminate)
job.last_run = data["last_run"]
self.jobs[job_id] = StartRailJob(job_id, job, op_type)
def __run_scheduler(self):
while not self._stop_event.is_set():
schedule.run_pending()
time.sleep(1)
def stop_scheduler(self):
aprint("Stopping the scheduler...", submodule_name=SUBMODULE_NAME)
self._stop_event.set()
self.scheduler_thread.join()
# =============================================
# =========| ADD/REMOVE FUNCTIONS | ===========
# =============================================
def add_new_schedule(self, time_str, operation_type: OperationTypes):
parsed_time = self.__parse_time_format(time_str)
# If parsed time is invalid
if parsed_time == None:
aprint(f"Cannot parse time: {time_str}", log_type=LogType.ERROR)
raise SRExit()
# If scheduled job already exist at the new job time
for job_id, job in self.jobs.items():
if job.next_run.strftime(TIME_FORMAT) == parsed_time:
aprint(f"{Printer.to_lightred(f'Job (ID {job_id}) is already scheduled at time {parsed_time}.')}")
return
if operation_type == OperationTypes.START:
job = schedule.every().day.at(parsed_time).do(self.starrail.start)
elif operation_type == OperationTypes.END:
job = schedule.every().day.at(parsed_time).do(self.starrail.terminate)
sr_job_id = self.__get_next_job_id()
sr_job = StartRailJob(sr_job_id, job, operation_type)
self.jobs[sr_job_id] = sr_job
aprint(f"New scheduled job (ID {sr_job_id}) has been added to the scheduler successfully!")
self.show_schedules()
# Save new schedules list into config
self.schedule_config.save_schedule([job.to_dict() for job in self.jobs.values()])
def remove_schedule(self):
if len(self.jobs) == 0:
aprint("No scheduled job to remove.")
return
self.show_schedules()
aprint(f"Which job would you like to remove? [ID 1{'-' + str(len(self.jobs)) if len(self.jobs) > 1 else ''}] ", end="")
user_input_id = input("")
job_id = self.__parse_id(user_input_id)
sr_job: StartRailJob = self.__get_job_with_id(job_id)
# Removing job from schedule
aprint(f"Removing job (ID {job_id})...", end="\r")
schedule.cancel_job(sr_job.schedule_job)
# Removing job from cache
del self.jobs[job_id]
# Reset job IDs after deletion
new_schedule_dict = dict()
for new_job_id, (old_job_id, sr_job) in enumerate(self.jobs.items()):
sr_job.job_id = new_job_id+1
new_schedule_dict[new_job_id+1] = sr_job
self.jobs = new_schedule_dict
# Saving jobs to config
self.schedule_config.save_schedule([job.to_dict() for job in self.jobs.values()])
aprint(f"Job (ID {job_id}) removed successfully.")
def show_schedules(self):
headers = ["ID", "Type", "Action", "Next Run", "Last Run"]
headers = [Printer.to_lightpurple(title) for title in headers]
payload = []
for job_id, job in self.jobs.items():
payload.append([job_id, "Scheduled Job", job.op_type.value, job.next_run, "None" if job.last_run == None else job.last_run])
tab = tabulate.tabulate(payload, headers)
print("\n" + tab + "\n", flush=True)
def clear_schedules(self):
if len(self.jobs) == 0:
aprint("No scheduled job to clear.")
return
self.show_schedules()
aprint(f"Are you sure you want to clear all {len(self.jobs)} schedules? [y/n] ", end="")
user_input = input("").lower().strip()
if user_input == "y":
for job_id, sr_job in self.jobs.items():
aprint(f"Canceling schedule job (ID {job_id})...", end=" ")
schedule.cancel_job(sr_job.schedule_job)
print("Done")
time.sleep(0.1)
self.jobs.clear()
self.schedule_config.save_schedule([])
aprint("All scheduled jobs have been canceled successfully.")
else:
aprint("Clear operation canceled.")
# =============================================
# ===========| HELPER FUNCTIONS | =============
# =============================================
def __parse_time_format(self, str_time: str):
str_time = str_time.strip().lower()
match = re.search("[0-9]+:[0-9]+:[0-9]+", str_time)
if match:
return match.group(0)
match2 = re.search("[0-9]+:[0-9]+", str_time)
if match2:
return f"{match2.group(0)}:00"
try:
digit_time = int(str_time)
return f"{str_time}:00:00"
except ValueError:
pass
return None
def __parse_id(self, str_id: str):
try:
str_id = int(str_id)
except TypeError:
aprint(f"Invalid ID: {str_id}", log_type=LogType.ERROR)
raise SRExit()
return str_id
def __get_job_with_id(self, job_id: int):
try:
target_job = self.jobs[job_id]
return target_job
except KeyError:
aprint(f"Invalid ID: {job_id}", log_type=LogType.ERROR)
raise SRExit()
def __get_next_job_id(self):
return len(self.jobs) + 1
# # # Usage example
# sr = HonkaiStarRail()
# scheduler = SRScheduler(sr)
# scheduler.add_new_schedule("17:52", OperationTypes.START) # Schedules ABC to run every 10 minutes
# scheduler.add_new_schedule("22:12", OperationTypes.END) # Schedules ABC to run every 10 minutes
# while True:
# user_input = input("Enter 'show' to display the schedule or 'exit' to quit: ").strip().lower()
# if user_input == 'show':
# scheduler.show_schedules()
# if user_input == 'remove':
# scheduler.remove_schedule()
# elif user_input == 'exit':
# scheduler.stop_scheduler()
# break
================================================
FILE: src/starrail/config/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/config/config_handler.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import json
from starrail.constants import *
from starrail.exceptions.exceptions import *
from starrail.utils.json_handler import JSONConfigHandler
from pathlib import Path
# TODO: Config is only loaded the first time when the StarRailConfig is initialized. This is an issue
# when the module is configured in the CLI since the loaded configuration persists in the CLI.
class StarRailConfig(JSONConfigHandler):
def __init__(self):
__starrail_config = os.path.join(os.path.abspath(os.path.dirname(__file__)), "starrail_config.json")
super().__init__(__starrail_config, dict)
__raw_config: dict
try:
__raw_config = self.LOAD_CONFIG()
except json.JSONDecodeError:
self.__reset_config()
__raw_config = self.LOAD_CONFIG()
# Config variables
self.instance_pid: int = None
self.root_path: Path = None # Root as D:\HoYoPlay\games
self.innr_path: Path = None # Inner as D:\HoYoPlay\games\Star Rail Games or D:\HoYoPlay\games\Game
self.game_path: Path = None # Game as D:\HoYoPlay\games\Star Rail Games\StarRail.exe
self.disclaimer: bool = False
# Load config into attributes
if __raw_config != None:
try:
self.instance_pid = __raw_config["instance"]["pid"]
self.root_path = __raw_config["static"]["root_path"]
self.innr_path = __raw_config["static"]["innr_path"]
self.game_path = __raw_config["static"]["game_path"]
self.disclaimer = __raw_config["static"]["disclaimer"]
if self.root_path != None:
self.root_path = Path(self.root_path)
if self.innr_path != None:
self.innr_path = Path(self.innr_path)
if self.game_path != None:
self.game_path = Path(self.game_path)
except KeyError:
self.__reset_config()
else:
self.__reset_config()
# ==================================================
# ============== | UTILITY FUNCTIONS | =============
# ==================================================
def save_current_config(self):
self.SAVE_CONFIG(
{
"instance": {
"pid": self.instance_pid
},
"static": {
"root_path": str(self.root_path),
"innr_path": str(self.innr_path),
"game_path": str(self.game_path),
"disclaimer": self.disclaimer
}
}
)
def full_configured(self) -> bool:
return self.path_configured() and self.disclaimer_configured()
def path_configured(self) -> bool:
if isinstance(self.game_path, str):
self.game_path = Path(self.game_path)
if isinstance(self.innr_path, str):
self.innr_path = Path(self.innr_path)
if isinstance(self.root_path, str):
self.root_path = Path(self.root_path)
if isinstance(self.root_path, Path) and isinstance(self.game_path, Path) and isinstance(self.innr_path, Path):
return self.game_path.exists() and self.root_path.exists() and self.innr_path.exists()
return False
def disclaimer_configured(self) -> bool:
return self.disclaimer
# ==================================================
# ============== | HELPER FUNCTIONS | ==============
# ==================================================
def set_path(self, game_path: str):
self.game_path = Path(game_path)
self.innr_path = os.path.dirname(game_path)
self.root_path = Path(os.path.dirname(os.path.dirname(game_path)))
def __reset_config(self):
self.SAVE_CONFIG(
{
"instance": {
"pid": None
},
"static": {
"root_path": None,
"innr_path": None,
"game_path": None,
"disclaimer": False
}
}
)
self.game_path = None
self.innr_path = None
self.root_path = None
self.disclaimer = False
================================================
FILE: src/starrail/constants.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
# ==============================================
# ==========| MODULE/GAME CONSTANTS | ==========
# ==============================================
BASENAME = "StarRail CLI"
SHORTNAME = "StarRail"
COMMAND = "starrail"
VERSION = "1.0.5"
VERSION_DESC = "Beta"
DEVELOPMENT = VERSION_DESC.lower() != "stable"
TIME_FORMAT = "%H:%M:%S"
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
GAME_NAME = "Honkai: Star Rail"
GAME_FILENAME = "StarRail.exe"
GAME_FILE_PATH = f"Star Rail/Game/{GAME_FILENAME}"
GAME_FILE_PATH_NEW = f"Star Rail Games/{GAME_FILENAME}"
AUTHOR = "Kevin L."
AUTHOR_DETAIL = f"{AUTHOR} - kevinliu@vt.edu - Github: ReZeroE"
REPOSITORY = "https://github.com/ReZeroE/StarRail"
ISSUES = f"{REPOSITORY}/issues"
HOMEPAGE_URL = "https://hsr.hoyoverse.com/en-us/home"
HOMEPAGE_URL_CN = "https://sr.mihoyo.com/"
HOYOLAB_URL = "https://www.hoyolab.com/home"
YOUTUBE_URL = "https://www.youtube.com/channel/UC2PeMPA8PAOp-bynLoCeMLA"
BILIBILI_URL = "https://space.bilibili.com/1340190821"
# ==============================================
# =============| OTHER CONSTANTS | =============
# ==============================================
CURSOR_UP_ANSI = "\033[A"
# See utils/game_detector for details on "Weak Match"
MIN_WEAK_MATCH_EXE_SIZE = 0.5 # megabytes
# ==============================================
# ==================| PATHS | ==================
# ==============================================
HOME_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
__PYTHON_MODULE_PATH = os.path.join(HOME_DIRECTORY, "Lib", "site-packages", "starrail")
if os.path.exists(__PYTHON_MODULE_PATH):
# If the module is built using `pip install .`, then the path would be something like C:/PythonXXX/Lib/site-packages/<package_name>
HOME_DIRECTORY = __PYTHON_MODULE_PATH
STARRAIL_DIRECTORY = __PYTHON_MODULE_PATH
else:
# Else if the module is built locally using `pip install -e .`, then the path will be the local directory structure
STARRAIL_DIRECTORY = os.path.join(HOME_DIRECTORY, "src", "starrail")
# ==============================================
# =================| GLOBALS | =================
# ==============================================
# Global variables to identify whether the program is in CLI mode
# MUST BE USED AS constants.CLI_MODE (only this accesses the re-bind value)
CLI_MODE = False
# ==============================================
# ================| RECORDER | =================
# ==============================================
# Recording window size preset:
RECORDER_WINDOW_INFO = {
"width": 600,
"height": 100,
"border-radius": 10
}
# Recording window preset size calibrated using:
CALIBRATION_MONITOR_INFO = {
"width": 3840,
"height": 2160
}
# ==============================================
# ==================| OTHER | ==================
# ==============================================
WEBCACHE_IGNORE_FILETYPES = [
".js",
".css",
".png",
".jpg",
".jpeg"
]
from pynput.keyboard import Key
PYNPUT_KEY_MAPPING = {
'Key.esc' : Key.esc,
'Key.space' : Key.space,
'Key.backspace' : Key.backspace,
'Key.enter' : Key.enter,
'Key.tab' : Key.tab,
'Key.caps_lock' : Key.caps_lock,
'Key.shift' : Key.shift,
'Key.ctrl' : Key.ctrl,
'Key.alt' : Key.alt,
'Key.delete' : Key.delete,
'Key.end' : Key.end,
'Key.home' : Key.home,
'Key.f1' : Key.f1,
'Key.f2' : Key.f2,
'Key.f3' : Key.f3,
'Key.f4' : Key.f4,
'Key.f5' : Key.f5,
'Key.f6' : Key.f6,
'Key.f7' : Key.f7,
'Key.f8' : Key.f8,
'Key.f9' : Key.f9,
'Key.f10' : Key.f10,
'Key.f11' : Key.f11,
'Key.f12' : Key.f12,
'Key.page_down' : Key.page_down,
'Key.page_up' : Key.page_up,
'Key.up' : Key.up,
'Key.down' : Key.down,
'Key.left' : Key.left,
'Key.right' : Key.right,
'Key.media_play_pause' : Key.media_play_pause,
'Key.media_volume_mute' : Key.media_volume_mute,
'Key.media_volume_up' : Key.media_volume_up,
'Key.media_volume_down' : Key.media_volume_down,
'Key.media_previous' : Key.media_previous,
'Key.media_next' : Key.media_next,
'Key.insert' : Key.insert,
'Key.menu' : Key.menu,
'Key.num_lock' : Key.num_lock,
'Key.pause' : Key.pause,
'Key.print_screen' : Key.print_screen,
'Key.scroll_lock' : Key.scroll_lock
}
================================================
FILE: src/starrail/controllers/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/controllers/automation_controller.py
================================================
import os
import sys
import time
import tabulate
from starrail.utils.utils import *
from starrail.automation.units.sequence import AutomationSequence
from starrail.automation.recorder import AutomationRecorder
from starrail.automation.config.automation_config_handler import StarRailAutomationConfig
from starrail.controllers.star_rail_app import HonkaiStarRail
class StarRailAutomationController:
def __init__(self, starrail_instance: HonkaiStarRail):
self.automation_config = StarRailAutomationConfig()
self.automation_sequences: dict[int, AutomationSequence] = self.__load_all_sequences()
self.starrail = starrail_instance # Only used by recorder to identify if the game is focused.
def __load_all_sequences(self):
sequence_list: list[AutomationSequence] = []
sequence_config_list = self.automation_config.load_all_automations()
for config in sequence_config_list:
sequence = AutomationSequence.parse_config(config)
sequence_list.append(sequence)
sequence_list.sort(key=lambda seq: seq.date_created)
return {idx+1: sequence for idx, sequence in enumerate(sequence_list)}
def verbose_general_usage(self):
headers = [Printer.to_lightpurple(title) for title in ["Example Command", "Description"]]
data = [
[color_cmd("automation record"), "Create and record a new automation sequence (macros)"],
[color_cmd("automation show"), "List all recorded automation sequences"],
[color_cmd("automation run"), "Run a recorded automation sequence"],
[color_cmd("automation remove"), "Delete a recorded automated sequence"],
[color_cmd("automation clear"), "Delete all recorded automation sequences"],
]
tab = tabulate.tabulate(data, headers)
print("\n" + tab + "\n")
# =============================================
# ============| FETCH AUTOMATION | ============
# =============================================
def get_all_sequences(self, list_format=False):
if list_format:
return list(self.automation_sequences.values())
return self.automation_sequences
def get_sequence(self, sequence_name: str):
target_sequence_name = self.__reformat_sequence_name(sequence_name)
for sequence in self.automation_sequences.values():
if sequence.sequence_name == target_sequence_name:
return sequence
return None
def get_sequence_with_id(self, sequence_id: int):
try:
return self.automation_sequences[sequence_id]
except KeyError:
return None
# =============================================
# ============| DRIVER FUNCTIONS | ============
# =============================================
def run_requence(self, sequence_name=None):
if len(self.automation_sequences) == 0:
aprint(f"{Printer.to_lightred('No automation sequence has been recorded.')}")
self.verbose_general_usage()
return
sequence = None
if sequence_name:
r_sequence_name = self.__reformat_sequence_name(sequence_name)
sequence = self.get_sequence(r_sequence_name)
if sequence == None:
aprint(f"Invalid input. No sequence with name {sequence_name}.")
raise SRExit()
else:
self.show_sequences()
aprint(f"Which sequence would you like to run? ({self.get_range_string()}) ", end="")
user_input_id = input("")
sequence = self.get_sequence_with_id(self.tryget_int(user_input_id))
if sequence == None:
aprint(f"Invalid input. No sequence with ID {user_input_id}.")
raise SRExit()
aprint(f"Run automation '{sequence.sequence_name}' ({Printer.to_lightgrey(f'approximately {sequence.get_runtime()} seconds')})? [y/n] ", end="")
user_input = input("").strip().lower()
if user_input != "y": return
aprint(f"Automation run ready start.\n{Printer.to_lightblue(' - To start')}: Focus on the game and the automation will automatically start.")
while True:
if self.starrail.is_focused():
time.sleep(1)
break
else:
time.sleep(0.5)
sequence.execute()
aprint("Automation sequence run complete! ")
def show_sequences(self):
headers = [Printer.to_lightpurple(title) for title in ["ID", "Sequence Name", "Date Created", "Runtime", "Actions Count"]]
data = []
for seq_id, sequence in self.automation_sequences.items():
row = [Printer.to_lightblue(seq_id), sequence.sequence_name, sequence.date_created, f"{sequence.get_runtime()} seconds", len(sequence.actions)]
data.append(row)
tab = tabulate.tabulate(data, headers)
print("\n" + tab + "\n", flush=True)
def record_sequences(self, sequence_name=None):
if sequence_name == None:
aprint("New sequence name: ", end="")
sequence_name = input("").strip()
# Initialize empty automation sequence for storing recording
NEW_SEQUENCE_NAME = self.__reformat_sequence_name(sequence_name)
if self.__sequence_already_exist(NEW_SEQUENCE_NAME):
aprint(f"Sequence with name '{NEW_SEQUENCE_NAME}' {Printer.to_lightred('already exist')}.")
raise SRExit()
recording_sequence = AutomationSequence(NEW_SEQUENCE_NAME)
recording_sequence.set_date_created_to_current()
# Start recording
try:
action_recorder = AutomationRecorder(recording_sequence, self.starrail)
action_recorder.record(start_on_callback=True)
except Exception as ex:
aprint(f"Uncaught Error (during recording): {ex}")
raise SRExit()
# TODO: Auto refactor the recorded sequence such that consecutive holds are merged.
# Convert recorded sequence into JSON and save it as config
json_sequence = recording_sequence.to_json()
success = self.automation_config.save_automation(recording_sequence.sequence_name, json_sequence)
assert(success == True)
# Cache recorded sequence in memory
sequence_id = self.get_next_sequence_id()
self.automation_sequences[sequence_id] = recording_sequence
aprint(f"Recording complete. Saved as '{Printer.to_light_blue(recording_sequence.sequence_name)}' (ID {sequence_id}).\n{Printer.to_lightblue(' - To view')}: {color_cmd('automation show')}\n{Printer.to_lightblue(' - To run')}: {color_cmd(f'automation run')}")
return recording_sequence
def delete_sequence(self):
if len(self.automation_sequences) == 0:
aprint("No automation sequence has been recorded.")
raise SRExit()
self.show_sequences()
aprint(f"Which sequence would you like to delete? ({self.get_range_string()}) ", end="")
user_input_id = input("")
seq_id = self.tryget_int(user_input_id)
sequence = self.get_sequence_with_id(seq_id)
if sequence == None:
aprint(f"Invalid input. No sequence with ID {user_input_id}.")
raise SRExit()
self.automation_config.delete_automation(sequence.sequence_name)
del self.automation_sequences[seq_id]
aprint(f"Automation sequence `{sequence.sequence_name}` has been deleted.")
def clear_sequences(self):
if len(self.automation_sequences) == 0:
aprint("No automation sequence has been recorded.")
raise SRExit()
self.show_sequences()
aprint(f"Are you sure you would like to clear all {len(self.automation_sequences)} automation sequences? [y/n] ", end="")
user_input = input("").strip().lower()
if user_input == "y":
for sequence in self.automation_sequences.values():
aprint(f"Deleting automation sequence '{sequence.sequence_name}' ...")
self.automation_config.delete_automation(sequence.sequence_name)
self.automation_sequences.clear()
aprint("All automation sequences have been deleted.")
# =============================================
# ============| HELPER FUNCTIONS | ============
# =============================================
def tryget_int(self, user_input_id):
if isinstance(user_input_id, str):
try:
seq_id = int(user_input_id)
except TypeError:
aprint(f"Invalid input: {user_input_id}", log_type=LogType.ERROR)
raise SRExit()
elif isinstance(user_input_id, int):
seq_id = user_input_id
return seq_id
def __sequence_already_exist(self, r_sequence_name):
for seq in self.automation_sequences.values():
if seq.sequence_name == r_sequence_name:
return True
return False
def get_range_string(self):
if len(self.automation_sequences) <= 1:
return "ID 1"
return f"ID {min(self.automation_sequences.keys())}-{max(self.automation_sequences.keys())}"
def get_next_sequence_id(self):
if len(self.automation_sequences) == 0:
return 1
return max(self.automation_sequences.keys()) + 1
def __reformat_sequence_name(self, sequence_name: str):
return sequence_name.strip().replace(" ", "-").lower()
================================================
FILE: src/starrail/controllers/c_click_controller.py
================================================
import time
import pyautogui
import random
from pynput import keyboard
from threading import Event
from starrail.exceptions.exceptions import SRExit
from starrail.utils.utils import aprint, Printer, is_admin, color_cmd
from starrail.constants import BASENAME
class ContinuousClickController:
def __init__(self):
self.thread_event = Event()
self.pause = False
self.click_count = 0
def click_continuously(
self,
count: int = -1,
interval: float = 1.0,
randomize_by: float = 0.0,
hold_time: float = 0.1,
start_after: float = 5.0,
quiet: bool = False
):
self.__verbose_start(count, interval, randomize_by, hold_time, start_after, quiet)
listener = keyboard.Listener(on_press=self.__on_press)
listener.start()
time.sleep(start_after)
try:
while True:
if self.click_count == count or self.thread_event.is_set():
listener.stop()
break
elif self.pause:
time.sleep(0.1)
continue
if not quiet:
self.__verbose_click(count)
self.__click(hold_time, interval, randomize_by)
except KeyboardInterrupt:
print("")
listener.stop()
time.sleep(1)
raise SRExit()
def __click(self, hold_time, interval, randomize_interval):
self.click_count += 1
pyautogui.mouseDown()
time.sleep(hold_time)
pyautogui.mouseUp()
time.sleep(interval)
time.sleep(random.uniform(0, randomize_interval))
def __verbose_start(self, count, interval, randomize_by, hold_time, start_after, quiet):
title_text = Printer.to_lightblue(f"Uniform clicking starting in {start_after} seconds.")
stop_text = Printer.to_lightblue(" - To stop: ") + "Press CTRL+C or ESC"
pause_text = Printer.to_lightblue(" - To pause: ") + "Press SPACE"
count_text = "INFINITE" if count == -1 else str(count)
count_text = Printer.to_purple(" - Total clicks: ") + count_text
interval_text = Printer.to_purple(" - Click interval: ") + f"{interval} seconds"
if randomize_by > 0:
interval_text += f" + random(0, {randomize_by}) seconds"
hold_time_text = Printer.to_purple(" - Hold duration: ") + f"{hold_time} seconds"
warning_text = ""
if not is_admin():
warning_text = Printer.to_lightred(f"\n\nNote: Currently not running {BASENAME} as admin. Clicks will not work in game applications.")
ccmd = color_cmd("starrail elevate", with_quotes=True)
warning_text += Printer.to_lightred(f"\n Run ") + ccmd + Printer.to_lightred(f" to elevate permissions to admin.")
aprint(
f"{title_text} \
\n{stop_text} \
\n{pause_text} \
\n\n{count_text} \
\n{hold_time_text} \
\n{interval_text} \
{warning_text}\n"
)
def __verbose_click(self, max_count):
buffer = " " * 10
x, y = pyautogui.position()
max_count_text = "INF" if max_count == -1 else max_count
aprint(f"[Count {self.click_count + 1}/{max_count_text}] Clicking ({x}, {y})...{buffer}", end="\r")
def __on_press(self, button):
if button == keyboard.Key.esc:
if self.click_count > 0:
print("")
aprint("Esc key pressed, stopping...")
self.thread_event.set()
exit() # After the event is set, the listener thread termiantes
if button == keyboard.Key.space:
self.pause = not self.pause
buffer = " " * 7
if self.pause:
aprint(f"Clicks paused. Press space again to start.{buffer}", end="\r")
else:
aprint(f"Clicks unpaused. Press space again to pause.{buffer}", end="\r")
================================================
FILE: src/starrail/controllers/star_rail_app.py
================================================
import os
import sys
import time
import psutil
import tabulate
import webbrowser
import subprocess
import configparser
from pathlib import Path
from starrail.constants import CURSOR_UP_ANSI
from starrail.utils.utils import *
from starrail.utils.process_handler import ProcessHandler
from starrail.config.config_handler import StarRailConfig
from starrail.controllers.webcache_controller import StarRailWebCacheController, StarRailWebCacheBinaryFile
from starrail.controllers.streaming_assets_controller import StarRailStreamingAssetsController, StarRailStreamingAssetsBinaryFile
from starrail.bin.loader.loader import Loader
class HonkaiStarRail:
def __init__(self):
self.config = StarRailConfig()
self.module_configured = self.config.full_configured()
self.webcache_controller = StarRailWebCacheController(self.config)
self.streaming_assets_controller = StarRailStreamingAssetsController(self.config)
# =============================================
# ============| DRIVER FUNCTIONS | ============
# =============================================
def start(self) -> bool:
aprint("Starting Honkai: Star Rail...")
starrail_proc = self.get_starrail_process()
# If the application is already running
if starrail_proc != None and starrail_proc.is_running():
ctext = Printer.to_lightred("already running")
aprint(f"[PID {starrail_proc.pid}] Application `{starrail_proc.name()}` is {ctext} in the background.")
raise SRExit()
# If the application is not running, then start the application
success = subprocess.Popen([str(self.config.game_path)], shell=True)
if self.wait_to_start():
starrail_proc = self.get_starrail_process()
aprint(f"[PID {starrail_proc.pid}] Honkai: Star Rail has started successfully!")
return True
aprint(f"Honkai: Star Rail failed to start due to an unknown reason.")
return False
def terminate(self) -> bool:
aprint("Terminating Honkai: Star Rail...", end="\r")
# If the cached proc PID is working
if self.config.instance_pid != None:
try:
starrail_proc = psutil.Process(self.config.instance_pid)
if self.proc_is_starrail(starrail_proc):
starrail_proc.terminate()
aprint("Honkai: Star Rail terminated successfully.")
return True
except psutil.NoSuchProcess:
self.config.instance_pid = None
self.config.save_current_config()
# If no PID is cached, find proc and termiante
starrail_proc = self.get_starrail_process()
if starrail_proc != None:
starrail_proc.terminate()
aprint("Honkai: Star Rail terminated successfully.")
return True
aprint(f"Honkai: Star Rail is currently {Printer.to_lightred('not running')}. ")
return False
def schedule(self):
# Scheduler implemented seperately in starrail/bin as of version 1.0.0
...
# =============================================
# ===========| UTILITY FUNCTIONS | ============
# =============================================
def show_status(self, live=False):
aprint("Loading status for the Honkai: Star Rail process...")
def print_status():
starrail_proc = self.get_starrail_process()
headers = [Printer.to_lightblue(title) for title in ["Title", "HSR Real-time Status"]]
data_dict = {
"Status" : "N/A",
"Process ID" : "N/A",
"Started On" : "N/A",
"CPU Percent" : "N/A",
"CPU Affinity" : "N/A",
"IO Operations" : "N/A",
"RAM Usage" : "N/A"
}
if starrail_proc != None: # Is running
data_dict["Status"] = bool_to_str(starrail_proc.is_running())
data_dict["Process ID"] = starrail_proc.pid
data_dict["Started On"] = DatetimeHandler.epoch_to_time_str(starrail_proc.create_time())
data_dict["CPU Percent"] = f"{starrail_proc.cpu_percent(1)}%"
data_dict["CPU Affinity"] = ",".join([str(e) for e in starrail_proc.cpu_affinity()])
data_dict["IO Operations"] = f"Writes: {starrail_proc.io_counters().write_count}, Reads: {starrail_proc.io_counters().read_count}"
data_dict["RAM Usage"] = f"{round(psutil.virtual_memory()[3]/1000000000, 4)} GB"
else:
data_dict["Status"] = bool_to_str(False)
# All other options remain as N/A
data_list = [[Printer.to_lightpurple(k), v] for k, v in data_dict.items()]
table = tabulate.tabulate(data_list, headers=headers)
print("\n" + table + "\n")
if live:
print(CURSOR_UP_ANSI * (len(data_list)+5))
try:
if live:
# TODO: Force CLI mode before allowing live status
while True:
print_status()
else:
print_status()
except KeyboardInterrupt:
aprint("Status reading stopped.")
raise SRExit()
def show_config(self):
# print(Printer.to_lightpurple("\n - Game Configuration Table -"))
headers = [Printer.to_lightblue(title) for title in ["Title", "Details", "Relevant Command"]]
data = [
["Game Version", self.fetch_game_version(), color_cmd("starrail version")],
["Game Executable", os.path.normpath(self.config.game_path), color_cmd("starrail start/stop")],
["Game Screenshots", self.__get_screenshot_path(), color_cmd("starrail screenshots")],
["Game Logs", self.__get_log_path(), color_cmd("starrail game-logs")],
["Game (.exe) SHA256", HashCalculator.SHA256(self.config.game_path) if self.config.game_path.exists() else None, ""]
]
for row in data:
row[0] = Printer.to_lightpurple(row[0])
table = tabulate.tabulate(data, headers=headers)
print("\n" + table + "\n")
def screenshots(self):
aprint("Opening the screenshots directory...", end="\r")
screenshot_path = self.__get_screenshot_path()
if not os.path.isdir(screenshot_path):
aprint(f"No screenshots taken.{" "*20}")
return
os.startfile(screenshot_path)
aprint(f"Screenshots directory opened in the File Explorer ({Printer.to_lightgrey(screenshot_path)}).")
def logs(self):
aprint("Opening the logs directory...", end="\r")
logs_path = self.__get_log_path()
if not os.path.isdir(logs_path):
aprint("No logs directory available locally.")
return
os.startfile(logs_path)
aprint(f"Logs directory opened in the File Explorer ({Printer.to_lightgrey(logs_path)}).")
def show_pulls(self):
aprint("Showing the pull history page...", end="\r")
cache_urls = self.webcache_controller.get_events_cache()
if cache_urls == None:
aprint("No pull history cache found locally.")
return
for url in cache_urls:
webbrowser.open(url)
aprint("All pages opened successfully. ")
def verbose_play_time(self):
sr_proc = self.get_starrail_process()
if sr_proc == None:
aprint("Honkai: Star Rail is currently not running.")
return
ctime = sr_proc.create_time()
time_delta = datetime.now() - DatetimeHandler.epoch_to_datetime(ctime)
aprint(f"{Printer.to_lightblue('Session Time:')} {DatetimeHandler.seconds_to_time_str(time_delta.seconds)}")
# =============================================
# ==========| WEBCACHE FUNCTIONS | ============
# =============================================
def webcache_announcements(self):
aprint("Decoding announcement webcache...")
cache_urls = self.webcache_controller.get_announcements_cache()
if cache_urls == None:
aprint("No announcement webcache found.")
else:
self.__print_cached_urls(cache_urls)
def webcache_events(self):
aprint("Decoding events/pulls webcache...")
cache_urls = self.webcache_controller.get_events_cache()
if cache_urls == None:
aprint("No events webcache found.")
else:
self.__print_cached_urls(cache_urls)
def webcache_all(self):
e_cache_urls = self.webcache_controller.get_events_cache()
a_cache_urls = self.webcache_controller.get_announcements_cache()
if e_cache_urls == None or len(e_cache_urls) == 0 and \
a_cache_urls == None or len(a_cache_urls) == 0:
aprint("No available webcache found.")
return
else:
print("")
new_line = False
if e_cache_urls != None:
new_line = True
print(Printer.to_lightblue(" - Events/Pulls Web Cache -"))
self.__print_cached_urls(e_cache_urls)
if a_cache_urls != None:
if new_line:
print("")
print(Printer.to_lightblue(" - Announcements Web Cache -"))
self.__print_cached_urls(a_cache_urls)
def __print_cached_urls(self, url_list):
for idx, url in enumerate(url_list):
print(f"[{Printer.to_lightpurple(f'URL {idx+1}')}] " + url)
# =================================================
# ========| STREAMING ASSETS FUNCTIONS | ==========
# =================================================
def streaming_assets(self):
aprint("Decoding streaming assets...")
headers = [Printer.to_lightblue(title) for title in ["Title", "Details (binary)"]]
sa_binary_dict = self.streaming_assets_controller.get_sa_binary_version()
sa_client_dict = self.streaming_assets_controller.get_sa_client_config()
sa_dev_dict = self.streaming_assets_controller.get_sa_dev_config()
master_dict = merge_dicts(sa_binary_dict, sa_client_dict, sa_dev_dict)
if len(master_dict) > 0:
master_list = [[Printer.to_lightpurple(title), "\n".join(data) if isinstance(data, list) else data] for title, data in master_dict.items() if title != "Other"]
if "Other" in master_dict.keys():
data = master_dict["Other"]
master_list.append([Printer.to_lightpurple("Other"), "\n".join(data) if isinstance(data, list) else data])
table = tabulate.tabulate(master_list, headers)
print("\n" + table + "\n")
# =============================================
# =========| PATH HELPER FUNCTIONS | ==========
# =============================================
def __get_screenshot_path(self):
return os.path.normpath(os.path.join(self.config.innr_path, "StarRail_Data", "ScreenShots"))
def __get_log_path(self):
return os.path.normpath(os.path.join(self.config.innr_path, "logs"))
def __get_game_config_path(self):
return os.path.normpath(os.path.join(self.config.innr_path, "config.ini"))
def fetch_game_version(self):
config = configparser.ConfigParser()
config.read(self.__get_game_config_path())
game_version = "N/A"
try:
game_version = config.get('general', 'game_version')
except configparser.NoSectionError:
try:
game_version = config.get('General', 'game_version')
except configparser.NoSectionError:
pass
return game_version
# =============================================
# ======| START/STOP HELPER FUNCTIONS | =======
# =============================================
def wait_to_start(self, timeout = 30) -> bool:
starting_time = time.time()
while time.time() - starting_time < timeout:
if self.is_running():
return True
time.sleep(0.5)
return False
def is_running(self) -> bool:
return self.get_starrail_process() != None
def is_focused(self) -> bool:
hsr_proc = self.get_starrail_process()
if hsr_proc == None:
return False
focused_pid = ProcessHandler.get_focused_pid()
if hsr_proc.pid == focused_pid:
return True
return False
def get_starrail_process(self) -> psutil.Process:
EXE_BASENAME = os.path.basename(self.config.game_path)
for p in psutil.process_iter(['pid', 'name', 'exe']):
# name == EXE_BASENAME is for optimization only (proceed only if filename is the same)
if p.info["name"] == EXE_BASENAME:
try:
starrail_proc = psutil.Process(p.info['pid'])
if self.proc_is_starrail(starrail_proc):
# If the cached PID isn't the current PID, reset it
if self.config.instance_pid != starrail_proc.pid:
self.config.instance_pid = starrail_proc.pid
self.config.save_current_config()
return starrail_proc
except psutil.NoSuchProcess:
continue
return None
def proc_is_starrail(self, starrail_proc: psutil.Process):
return starrail_proc.is_running() and Path(starrail_proc.exe()) == Path(self.config.game_path)
================================================
FILE: src/starrail/controllers/streaming_assets_controller.py
================================================
import os
import re
import sys
from pathlib import Path
from enum import Enum
from starrail.config.config_handler import StarRailConfig
from starrail.utils.utils import aprint, Printer
from starrail.constants import WEBCACHE_IGNORE_FILETYPES, GAME_FILE_PATH, GAME_FILE_PATH_NEW
from starrail.utils.binary_decoder import StarRailBinaryDecoder
SUBMODULE_NAME = "SR-SAC"
class StarRailStreamingAssetsBinaryFile(Enum):
SA_BinaryVersion = "BinaryVersion.bytes"
SA_ClientConfig = "ClientConfig.bytes"
SA_DevConfig = "DevConfig.bytes"
class StarRailStreamingAssetsController:
def __init__(self, starrail_config: StarRailConfig):
self.starrail_config = starrail_config
self.binary_decoder = StarRailBinaryDecoder()
# =============================================
# ============| DRIVER FUNCTIONS | ============
# =============================================
def get_decoded_streaming_assets(self, sa_binary_file: StarRailStreamingAssetsBinaryFile):
decoded_strings = self.decode_streaming_assets(sa_binary_file)
if decoded_strings == None:
return None
filtered_dict = self.parse_webcache(decoded_strings, sa_binary_file)
return filtered_dict
def get_sa_binary_version(self):
return self.get_decoded_streaming_assets(StarRailStreamingAssetsBinaryFile.SA_BinaryVersion)
def get_sa_client_config(self):
return self.get_decoded_streaming_assets(StarRailStreamingAssetsBinaryFile.SA_ClientConfig)
def get_sa_dev_config(self):
return self.get_decoded_streaming_assets(StarRailStreamingAssetsBinaryFile.SA_DevConfig)
# =============================================
# ==========| SUBDRIVER FUNCTIONS | ===========
# =============================================
def decode_streaming_assets(self, sa_binary_file: StarRailStreamingAssetsBinaryFile):
file_path = os.path.join(self.starrail_config.innr_path, "StarRail_Data", "StreamingAssets", sa_binary_file.value)
if not os.path.isfile(file_path):
aprint(Printer.to_lightred(f"Decoder cannot locate streaming assets file '{file_path}'."))
return
aprint(f"Decoding {Printer.to_lightgrey(file_path)} ...", submodule_name=SUBMODULE_NAME)
try:
return self.binary_decoder.decode_raw_binary_file(file_path)
except PermissionError as ex:
aprint(f"{Printer.to_lightred('Permission denied.')}")
return None
def parse_webcache(self, decoded_strings, sa_binary_file: StarRailStreamingAssetsBinaryFile):
data_dict = dict()
if sa_binary_file == StarRailStreamingAssetsBinaryFile.SA_BinaryVersion:
for string in decoded_strings:
string = string.strip()
if re.search("PRODWin[0-9]{1}.[0-9]{1}.[0-9]{1}", string) != None:
data_dict["Detailed Version"] = string
elif re.search("V[0-9]{1}.[0-9]{1}", string) != None:
data_dict["Version"] = string
elif re.search("[0-9]{8}-[0-9]{4}", string) != None:
data_dict["Datetime String"] = string
else:
try:
endpoints = data_dict["Other"]
endpoints.append(string)
data_dict["Other"] = endpoints
except KeyError:
data_dict["Other"] = [string]
if sa_binary_file == StarRailStreamingAssetsBinaryFile.SA_ClientConfig:
for string in decoded_strings:
string = string.strip()
match = re.search("https.*", string)
if string.startswith("com."):
data_dict["Application Identifier"] = string
elif match != None:
try:
endpoints = data_dict["Server Endpoints"]
endpoints.append(match.group(0))
data_dict["Service Endpoints"] = endpoints
except KeyError:
data_dict["Service Endpoints"] = [match.group(0)]
else:
try:
endpoints = data_dict["Unknown"]
endpoints.append(string)
data_dict["Unknown"] = endpoints
except KeyError:
data_dict["Unknown"] = [string]
if sa_binary_file == StarRailStreamingAssetsBinaryFile.SA_DevConfig:
for string in decoded_strings:
string = string.strip()
if re.search("V[0-9]{1}.[0-9]{1}", string) != None:
if "EngineRelease" in string:
data_dict["Engine Version"] = string
else:
data_dict["Unknown Version"] = string
else:
try:
endpoints = data_dict["Unknown"]
endpoints.append(string)
data_dict["Unknown"] = endpoints
except KeyError:
data_dict["Unknown"] = [string]
if len(data_dict) > 0:
return data_dict
return None
================================================
FILE: src/starrail/controllers/web_controller.py
================================================
import webbrowser
from starrail.constants import HOMEPAGE_URL, HOMEPAGE_URL_CN, HOYOLAB_URL, YOUTUBE_URL, BILIBILI_URL
from starrail.utils.utils import aprint, Printer
class StarRailWebController:
def __init__(self):
pass
def homepage(self, cn=False):
if cn:
aprint(f"Opening Honkai: Star Rail's official home page (CN)...\n - {Printer.to_lightgrey(HOMEPAGE_URL_CN)}")
webbrowser.open(HOMEPAGE_URL_CN)
else:
aprint(f"Opening Honkai: Star Rail's official home page...\n - {Printer.to_lightgrey(HOMEPAGE_URL)}")
webbrowser.open(HOMEPAGE_URL)
def hoyolab(self):
aprint(f"Opening Honkai: Star Rail's HoyoLab page...\n - {Printer.to_lightgrey(HOYOLAB_URL)}")
webbrowser.open(HOYOLAB_URL)
def youtube(self):
aprint(f"Opening Honkai: Star Rail's Youtube page...\n - {Printer.to_lightgrey(YOUTUBE_URL)}")
webbrowser.open(YOUTUBE_URL)
def bilibili(self):
aprint(f"Opening Honkai: Star Rail's BiliBili page...\n - {Printer.to_lightgrey(BILIBILI_URL)}")
webbrowser.open(BILIBILI_URL)
================================================
FILE: src/starrail/controllers/webcache_controller.py
================================================
import os
import re
import sys
import shutil
from pathlib import Path
from enum import Enum
from starrail.config.config_handler import StarRailConfig
from starrail.utils.utils import aprint, Printer
from starrail.constants import WEBCACHE_IGNORE_FILETYPES
from starrail.utils.binary_decoder import StarRailBinaryDecoder
SUBMODULE_NAME = "SR-WCC"
class StarRailWebCacheBinaryFile(Enum):
WEBCACHE_DATA0 = "data_0"
WEBCACHE_DATA1 = "data_1" # Anncouncements
WEBCACHE_DATA2 = "data_2" # Events/Pulls
class StarRailWebCacheController:
def __init__(self, starrail_config: StarRailConfig):
self.starrail_config = starrail_config
self.binary_decoder = StarRailBinaryDecoder()
# =============================================
# ============| DRIVER FUNCTIONS | ============
# =============================================
def get_decoded_webcache(self, webcache_binary_file: StarRailWebCacheBinaryFile):
decoded_strings = self.decode_webcache(webcache_binary_file)
if decoded_strings == None:
return None
filtered_urls = self.parse_webcache(decoded_strings, webcache_binary_file)
return filtered_urls
def get_announcements_cache(self):
return self.get_decoded_webcache(StarRailWebCacheBinaryFile.WEBCACHE_DATA1)
def get_events_cache(self):
return self.get_decoded_webcache(StarRailWebCacheBinaryFile.WEBCACHE_DATA2)
# =============================================
# ==========| SUBDRIVER FUNCTIONS | ===========
# =============================================
def decode_webcache(self, webcache_binary_file: StarRailWebCacheBinaryFile):
# Get webCache path (varying webcache versioning)
def get_webcache_path():
webcache_path = ""
webcache_semi_path = Path(os.path.join(self.starrail_config.innr_path, "StarRail_Data", "webCaches"))
try:
version_dirs = [d for d in webcache_semi_path.iterdir() if d.is_dir()]
if len(version_dirs) > 0:
version_dir = version_dirs[0]
webcache_path = os.path.join(self.starrail_config.innr_path, "StarRail_Data", "webCaches", version_dir, "Cache", "Cache_Data", webcache_binary_file.value)
return webcache_path
else:
return None
except Exception as ex:
return None
file_path = get_webcache_path()
if file_path == None or not os.path.isfile(file_path):
return None
aprint(f"Decoding {webcache_binary_file.value} ({Printer.to_lightgrey(file_path)}) ...", submodule_name=SUBMODULE_NAME)
try:
return self.binary_decoder.decode_raw_binary_file(file_path)
except PermissionError:
aprint(f"{Printer.to_lightred('Web cache is LOCKED.')} Web cache is only available when the game is not running.", submodule_name=SUBMODULE_NAME)
return None
def parse_webcache(self, decoded_strings, webcache_file: StarRailWebCacheBinaryFile):
if webcache_file == StarRailWebCacheBinaryFile.WEBCACHE_DATA0:
pass
if webcache_file == StarRailWebCacheBinaryFile.WEBCACHE_DATA1:
return self.filter_urls("webstatic.mihoyo.com/hkrpg/announcement", decoded_strings)
if webcache_file == StarRailWebCacheBinaryFile.WEBCACHE_DATA2:
return self.filter_urls("webstatic.mihoyo.com/hkrpg/event", decoded_strings)
return None
# =============================================
# ============| HELPER FUNCTIONS | ============
# =============================================
def filter_urls(self, target_sequence: str, decoded_strings):
# Filter out URLs without the target sequence and end with the ignoring file types
filtered_urls = []
for url in decoded_strings:
url = url.strip()
if target_sequence in url and not self.should_ignore(url):
match = re.search("https.*", url)
if match != None:
filtered_urls.append(match.group(0))
return filtered_urls
def should_ignore(self, url: str):
# Should ignore current URL because of its file type
for ignoring_file_type in WEBCACHE_IGNORE_FILETYPES:
if url.endswith(ignoring_file_type):
return True
return False
================================================
FILE: src/starrail/data/textfiles/disclaimer.txt
================================================
=====================================================================
================== | STARRAIL PACKAGE DISCLAIMER | ==================
=====================================================================
The "starrail" Python 3 module is an external CLI tool designed
to automate the gameplay of Honkai Star Rail. It is designed solely
interacts with the game through the existing user interface, and it
abides by the Fair Gaming Declaration set forth by COGNOSPHERE PTE.
LTD. The package is designed to provide a streamlined and efficient
way for users to interact with the game through features already
provided within the game, and it does not, in any way, intend
to damage the balance of the game or provide any unfair advantages.
The package does NOT modify any files in any way.
The creator(s) of this package has no relationship with MiHoYo, the
game's developer. The use of this package is entirely at the user's
own risk, and the creator accepts no responsibility for any damage or
loss caused by the package's use. It is the user's responsibility to
ensure that they use the package according to Honkai Star Rail's Fair
Gaming Declaration, and the creator accepts no responsibility for any
consequences resulting from its misuse, including game account
penalties, suspension, or bans.
Please note that according to MiHoYo's Honkai: Star Rail Fair Gaming
Declaration (https://hsr.hoyoverse.com/en-us/news/111244):
"It is strictly forbidden to use external plug-ins, game
accelerators/boosters, scripts, or any other third-party tools
that damage the balance of the game. Once discovered, COGNOSPHERE
PTE. LTD. (referred to as "we" henceforth) will take appropriate
actions depending on the severity and frequency of the offenses.
These actions include removing rewards obtained through such
infringements, suspending the game account, or permanently
banning the game account. Therefore, the user of this package
must be aware that the use of this package may result in the
above actions being taken against their game account by MiHoYo."
By using this package, the user agrees to ALL terms and conditions
and acknowledges that the creator will not be held liable for any
negative outcomes that may occur as a result of its use.
================================================
FILE: src/starrail/data/textfiles/webcache_explain.txt
================================================
Web cache for Honkai: Star Rail stores recent web data.
You can open URLs to view announcements, events, or pull status,
allowing quick access without loading into the game.
================================================
FILE: src/starrail/entrypoints/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/entrypoints/entrypoint_handler.py
================================================
import readline
import argparse
import getpass
import webbrowser
import tabulate
import subprocess
from starrail.constants import BASENAME, AUTHOR, VERSION, VERSION_DESC, AUTHOR_DETAIL, REPOSITORY, CURSOR_UP_ANSI, REPOSITORY, STARRAIL_DIRECTORY, ISSUES
from starrail.utils.utils import *
from starrail.utils.binary_decoder import StarRailBinaryDecoder
from starrail.utils.game_detector import StarRailGameDetector
from starrail.utils.perm_elevate import StarRailPermissionsHandler
from starrail.exceptions.exceptions import SRExit, StarRailBaseException
from starrail.controllers.star_rail_app import HonkaiStarRail
from starrail.controllers.web_controller import StarRailWebController
from starrail.controllers.automation_controller import StarRailAutomationController
from starrail.controllers.c_click_controller import ContinuousClickController
from starrail.bin.loader.loader import Loader
from starrail.bin.scheduler.starrail_scheduler import StarRailScheduler, OperationTypes
class StarRailEntryPointHandler:
def __init__(self):
self.star_rail = HonkaiStarRail()
self.web_controller = StarRailWebController()
self.scheduler = StarRailScheduler(self.star_rail) # Only the start/stop is used from the starrail instance
self.automation_controller = StarRailAutomationController(self.star_rail)
# ================================================
# ==================| PROJECT | ==================
# ================================================
def about(self, args):
headers = [Printer.to_lightblue(title) for title in ["Title", "Description"]]
data = [
["Module", BASENAME],
["Version", f"{VERSION_DESC}-{VERSION}"],
["Author", AUTHOR],
["Repository", REPOSITORY],
["Directory", STARRAIL_DIRECTORY]
]
for row in data:
row[0] = Printer.to_lightpurple(row[0])
print("\n" + tabulate.tabulate(data, headers=headers) + "\n")
def version(self, args):
headers = [Printer.to_lightblue(title) for title in ["Program", "Description", "Version"]]
data = [
["Honkai: Star Rail", "Game", self.star_rail.fetch_game_version()],
[BASENAME, "Module", f"{VERSION_DESC}-{VERSION}"]
]
for row in data:
row[0] = Printer.to_lightpurple(row[0])
print("\n" + tabulate.tabulate(data, headers=headers) + "\n")
def author(self, args):
aprint(AUTHOR_DETAIL)
def repo(self, args):
aprint(REPOSITORY)
if args.open:
webbrowser.open(REPOSITORY)
# =================================================
# ==================| GAME INFO | =================
# =================================================
def show_status(self, args):
self.star_rail.show_status(args.live)
def show_config(self, args):
self.star_rail.show_config()
def show_details(self, args):
self.star_rail.streaming_assets()
def play_time(self, args):
self.star_rail.verbose_play_time()
# =================================================
# ===============| LAUNCH DRIVER | ================
# =================================================
def start(self, args):
self.star_rail.start()
def stop(self, args):
self.star_rail.terminate()
def schedule(self, args):
def verbose_add_usage():
aprint(f"To schedule a new game start, run:\n{color_cmd('starrail schedule add --action start --time 10:30', True)}")
def verbose_general_usage():
headers = [Printer.to_lightpurple(title) for title in ["Example Command", "Description"]]
data = [
[color_cmd("schedule add --time 10:30 --action start"), "Schedule Honkai Star Rail to START at 10:30 AM"],
[color_cmd("schedule add --time 15:30 --action stop"), "Schedule Honkai Star Rail to STOP at 3:30 PM"],
[color_cmd("schedule remove"), "Remove an existing scheduled job"],
[color_cmd("schedule show"), "Show all scheduled jobs and their details"],
[color_cmd("schedule clear"), "Cancel all schedule jobs (irreversible)"]
]
tab = tabulate.tabulate(data, headers)
print("\n" + tab + "\n")
if args.subcommand == "add":
op_type = None
if args.action.lower().strip() == "start":
op_type = OperationTypes.START
elif args.action.lower().strip() == "stop":
op_type = OperationTypes.END
else:
verbose_add_usage()
return
if not args.time:
verbose_add_usage()
return
self.scheduler.add_new_schedule(args.time, op_type)
elif args.subcommand == "remove":
self.scheduler.remove_schedule()
elif args.subcommand == "show":
self.scheduler.show_schedules()
elif args.subcommand == "clear":
self.scheduler.clear_schedules()
elif args.subcommand == "help":
verbose_general_usage()
else:
if args.subcommand != None:
aprint(Printer.to_lightred(f"Unknown scheduling action: '{args.subcommand}'"))
verbose_general_usage()
# =================================================
# =================| AUTOMATION | =================
# =================================================
def automation(self, args):
def force_admin():
if not is_admin():
aprint(f"{Printer.to_lightred('Admin permission required for automation')}.\nWould you like to elevate the permissions to admin? [y/n] ", end="")
if input("").strip().lower() == "y":
StarRailPermissionsHandler.elevate()
raise SRExit()
else:
raise SRExit()
if args.action == "record":
force_admin()
self.automation_controller.record_sequences()
elif args.action == "run":
force_admin()
self.automation_controller.run_requence()
elif args.action == "show":
self.automation_controller.show_sequences()
elif args.action == "remove":
self.automation_controller.delete_sequence()
elif args.action == "clear":
self.automation_controller.clear_sequences()
else:
if args.action != None:
aprint(Printer.to_lightred(f"Unknown automation action: '{args.action}'"))
self.automation_controller.verbose_general_usage()
def click_continuously(self, args):
def force_admin(args):
if not is_admin():
aprint(f"{Printer.to_lightblue('Admin permission required for clicks to be registered in-game')}.\nWould you like to elevate the permissions to admin? [y/n] ", end="")
if input("").strip().lower() == "y":
StarRailPermissionsHandler.elevate([
"click",
"--clicks", args.clicks,
"--interval", args.interval,
"--randomize", args.randomize,
"--hold", args.hold,
"--delay", args.delay,
"--quiet" if args.quiet else ""
])
raise SRExit()
else:
raise SRExit()
cc_controller = ContinuousClickController()
force_admin(args)
cc_controller.click_continuously(args.clicks, args.interval, args.randomize, args.hold, args.delay, args.quiet)
# =================================================
# ==================| KEY URLS | ==================
# =================================================
def homepage(self, args):
self.web_controller.homepage(args.cn)
def hoyolab(self, args):
self.web_controller.hoyolab()
def youtube(self, args):
self.web_controller.youtube()
def bilibili(self, args):
self.web_controller.bilibili()
# ===================================================
# ===============| UTILITY FUNCTIONS | ==============
# ===================================================
def screenshots(self, args):
self.star_rail.screenshots()
def game_logs(self, args):
self.star_rail.logs()
def decode(self, args):
ascii_binary_decoder = StarRailBinaryDecoder()
ascii_binary_decoder.user_decode(args.path, args.min_length)
def pulls(self, args):
self.star_rail.show_pulls()
def webcache(self, args):
if not args.quiet:
print_webcache_explanation()
if args.announcements:
self.star_rail.webcache_announcements()
elif args.events:
self.star_rail.webcache_events()
else:
self.star_rail.webcache_all()
# =================================================
# =============| BLOCKING FUNCTIONS | =============
# =================================================
def configure(self, args):
if self.star_rail.config.full_configured():
aprint(Printer.to_lightgreen("Configuration Already Completed!"))
return
# os.system("cls")
if self.star_rail.config.disclaimer_configured() == False:
# print(Printer.to_skyblue("\n\n - STEP 1 OF 2. DISCLAIMER AGREEMENT - \n"))
step_two_text = " - STEP 1 OF 2. DISCLAIMER AGREEMENT - "
print(Printer.to_skyblue(f"\n\n {'='*len(step_two_text)}"))
print(Printer.to_skyblue(f" {step_two_text}"))
print(Printer.to_skyblue(f" {'='*len(step_two_text)}\n"))
print_disclaimer()
print(Printer.to_lightred("[IMPORTANT] Please read the disclaimer above before continuing!"))
if input("AGREE? [y/n] ").lower() == "y":
self.star_rail.config.disclaimer = True
self.star_rail.config.save_current_config()
else:
return
if self.star_rail.config.path_configured() == False:
step_two_text = "- STEP 2 OF 2. SET UP GAME PATH (StarRail.exe) -"
print(Printer.to_skyblue(f"\n\n {'='*len(step_two_text)}"))
print(Printer.to_skyblue(f" {step_two_text}"))
print(Printer.to_skyblue(f" {'='*len(step_two_text)}\n"))
logt = f"Auto Detecting Honkai: Star Rail ({Printer.to_lightgrey('this may take a while')})..."
with Loader(logt, end=None):
star_rail_game_detector = StarRailGameDetector()
game_path = star_rail_game_detector.find_game()
if game_path == None:
print(CURSOR_UP_ANSI, flush=True)
aprint(Printer.to_lightred("Cannot locate game Honkai: Star Rail.") + " "*15 + f"\nFile issue at '{ISSUES}' for more help.")
raise SRExit()
self.star_rail.config.set_path(game_path)
self.star_rail.config.save_current_config()
print(CURSOR_UP_ANSI, flush=True)
aprint("Game found at: " + game_path + " "*10)
aprint("Configuration Complete!")
# ===================================================
# ===============| HELPER FUNCTIONS | ===============
# ===================================================
def elevate(self, args):
StarRailPermissionsHandler.elevate(args.arguments)
# =================================================
# ===============| CLI DRIVERS | ==================
# =================================================
def print_title(self):
# columns, _ = shutil.get_terminal_size(fallback=(80, 20))
# bar = '▬' * (columns//1 - 12)
# bar = Printer.to_lightblue(f"▷ ▷ ▷ {bar} ◁ ◁ ◁")
# print(bar)
title = Printer.to_lightblue(
r"""
____ _ ____ _ _ ____ _ ___
/ ___|| |_ __ _ _ __| _ \ __ _(_) | / ___| | |_ _|
\___ \| __/ _` | '__| |_) / _` | | | | | | | | |
___) | || (_| | | | _ < (_| | | | | |___| |___ | |
|____/ \__\__,_|_| |_| \_\__,_|_|_| \____|_____|___|
""")
desc = Printer.to_purple("""A lightweight Command Line Utility For Honkai: Star Rail!""")
postfix = Printer.to_lightgrey(REPOSITORY)
author = Printer.to_lightgrey(f"By {AUTHOR}")
print(center_text(title))
print_centered(f"{desc}\n{postfix}\n{author}\n")
def print_init_help(self):
access_time = colored(f"Access Time: {DatetimeHandler.get_datetime_str()}", "dark_grey")
username = getpass.getuser()
isadmin = "ADMIN" if is_admin() else "USER"
quit_cmd = Printer.to_purple("exit")
cls_cmd = Printer.to_purple("clear")
help_cmd = Printer.to_purple("help")
dev_str = ""
# if DEVELOPMENT:
# dev_str = f"({Printer.to_lightred("Dev=True")}) {Printer.to_lightgrey("Development commands available.")}\n"
welcome_str = f"Welcome to the {BASENAME} Environment ({VERSION_DESC}-{VERSION})"
exit_str = f"Type '{quit_cmd}' to quit {SHORTNAME} CLI"
cls_str = f"Type '{cls_cmd}' to clear terminal"
help_str = f"Type '{help_cmd}' to display commands list"
print(f"{dev_str}{welcome_str}\n {help_str}\n {exit_str}\n {cls_str}\n")
def clear_screen(self):
os.system('cls')
def check_custom_commands(self, user_input: str):
# Return 0 to continue loop, 1 to short-circit loop and 'continue' loop, 2 to exit loop
if user_input.lower() in ['exit', 'quit']:
self.clear_screen()
return 2
if user_input.lower() in ['clear', "cls", "reset"]:
self.clear_screen()
self.print_title()
exit_cmd = color_cmd("exit", with_quotes=True)
print(f"Terminal cleared. Type {exit_cmd} to quit.")
return 1
if user_input.strip() == '':
return 1
return 0
def setup_key_bindings(self):
readline.parse_and_bind(r'"\C-w": backward-kill-word')
readline.parse_and_bind(r'"\C-a": beginning-of-line')
readline.parse_and_bind(r'"\C-e": end-of-line')
readline.parse_and_bind(r'"\C-u": unix-line-discard')
readline.parse_and_bind(r'"\C-k": kill-line')
readline.parse_and_bind(r'"\C-y": yank')
readline.parse_and_bind(r'"\C-b": backward-char')
readline.parse_and_bind(r'"\C-f": forward-char')
readline.parse_and_bind(r'"\C-p": previous-history')
readline.parse_and_bind(r'"\C-n": next-history')
readline.parse_and_bind(r'"\C-b": backward-word')
readline.parse_and_bind(r'"\C-f": forward-word')
def start_cli(self, parser: argparse.ArgumentParser):
prefilled = False
# =============| SET CLI MODE |==============
constants.CLI_MODE = True
# ============| SETUP READLINE |=============
self.setup_key_bindings()
# ==============| PRINT TITLE |==============
self.clear_screen()
self.print_title()
self.print_init_help()
cli_env_alert = False
# ================| CLI LOOP |================
while True:
try:
starrail_cli = colored(f"{DatetimeHandler.get_time_str()} StarRail", "dark_grey")
print(f"[{starrail_cli}] > ", end="", flush=True)
user_input = input().strip()
continue_loop = self.check_custom_commands(user_input)
if continue_loop == 1:
continue
elif continue_loop == 2:
break
# Verify that the stdout and stderr aren't closed
if sys.stdout.closed or sys.stderr.closed:
text = "STDOUT and STDERR" if sys.stdout.closed and sys.stderr.closed \
else "STDOUT" if sys.stdout.closed \
else "STDERR"
aprint(f"System {text} has been closed unexpectedly. Exiting {BASENAME} environment...")
sys.exit()
# =============| PARSE ARGUMENT |=============
if user_input.startswith(COMMAND):
user_input = user_input.lstrip(COMMAND).strip()
if not cli_env_alert:
print(Printer.to_lightgrey(f"Currently in the StarRail CLI environment. You may call `{user_input}` directly without the `{COMMAND}` prefix."))
cli_env_alert = not cli_env_alert
args = parser.parse_args(user_input.split())
if hasattr(args, 'func'):
try:
args.func(args)
except SRExit:
continue
else:
parser.print_help()
# ==============| ON EXCEPTION |==============
except KeyboardInterrupt:
# print("\nNote: Type 'exit' to quit.")
print("")
continue
except SystemExit:
continue
# ================================================
# ==================| FIREFLY | ==================
# ================================================
def firefly(self, args):
lines = [
" I dreamed of a scorched earth.",
" A new shoot sprouted from the earth.",
" It bloomed in the morning sun",
" ... and whispered to me.",
" Like fyreflies to a flame...",
" life begets death.",
"",
" -- Firefly S.A.M."
]
text = "\n".join(lines)
firefly_colors = [
Printer.to_pale_yellow,
Printer.to_light_blue,
Printer.to_teal,
Printer.to_turquoise,
# Printer.to_dark_teal,
Printer.to_turquoise,
Printer.to_teal,
Printer.to_light_blue,
Printer.to_pale_yellow,
]
result = ""
color_index = 0
for char in text:
if char != "\n":
color_func = firefly_colors[color_index % len(firefly_colors)]
result += color_func(char)
color_index += 1
else:
result += "\n"
print("\n" + result)
================================================
FILE: src/starrail/entrypoints/entrypoints.py
================================================
import os
import sys
import time
import argparse
start_time = time.time()
from starrail.entrypoints.entrypoint_handler import StarRailEntryPointHandler
from starrail.entrypoints.help_format_handler import HelpFormatHandler
from starrail.utils.utils import aprint, verify_platform, is_admin, Printer, color_cmd
from starrail.constants import COMMAND, DEVELOPMENT
from starrail.exceptions.exceptions import StarRailOSNotSupported, SRExit
class StarRailArgParser(argparse.ArgumentParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.groups = []
def add_group(self, title, description=None):
group = {'title': title, 'description': description, 'parsers': []}
self.groups.append(group)
return group
def add_parser_to_group(self, group, parser):
group['parsers'].append(parser)
def error(self, message):
if "invalid choice:" in message:
command = message.split("'")[1]
helpt = Printer.to_purple("starrail help")
aprint(f"Command not recognized: {command}\nType '{helpt}' for the commands list")
else:
parts = message.split("'")
if len(parts) > 1:
command = parts[1]
helpt = Printer.to_purple(f"{command} --help")
aprint(f"{message.capitalize()}\nType '{helpt}' for its arguments list")
else:
helpt = Printer.to_purple("<command> --help")
aprint(f"{message.capitalize()}\nType '{helpt}' for its arguments list")
self.exit(2)
def start_starrail():
os.system("") # Enables ANSI escape characters in terminal
entrypoint_handler = StarRailEntryPointHandler()
help_format_handler = HelpFormatHandler()
parser = StarRailArgParser(prog=COMMAND, description="StarRail CLI Module")
subparsers = parser.add_subparsers(dest='command', help='commands')
help_parser = subparsers.add_parser('help', help='Show this help message and exit')
help_parser.set_defaults(func=lambda args: help_format_handler.print_help(args, parser))
# ================================================
# ==================| PROJECT | ==================
# ================================================
about_group = parser.add_group('Package Info', 'Get information about the starrail package.')
abt_parser = subparsers.add_parser('about', help='Verbose all information about this module', description='Verbose all information about this module')
abt_parser.set_defaults(func=entrypoint_handler.about)
parser.add_parser_to_group(about_group, abt_parser)
version_parser = subparsers.add_parser('version', help='Verbose game AND module version', description='Verbose game AND module version')
version_parser.set_defaults(func=entrypoint_handler.version)
parser.add_parser_to_group(about_group, version_parser)
repo_parser = subparsers.add_parser('repo', help='Verbose module repository link', description='Verbose module repository link')
repo_parser.add_argument('--open', '-o', action='store_true', default=False, help='Open repository in web.')
repo_parser.set_defaults(func=entrypoint_handler.repo)
parser.add_parser_to_group(about_group, repo_parser)
author_parser = subparsers.add_parser('author', help='Verbose module author', description='Verbose module author')
author_parser.set_defaults(func=entrypoint_handler.author)
parser.add_parser_to_group(about_group, author_parser)
# ================================================
# =================| GAME INFO | =================
# ================================================
game_info_group = parser.add_group('Game Info', 'Get static and realtime information about the local installation of Honkai: Star Rail.')
show_status = subparsers.add_parser('status', help='Show real-time game status (game process)', description='Show real-time game status (game process)')
show_status.add_argument('--live', '-l', action='store_true', default=False, help='Show live game status (non-stop).')
show_status.set_defaults(func=entrypoint_handler.show_status)
parser.add_parser_to_group(game_info_group, show_status)
show_config = subparsers.add_parser('config', help='Show game configuration in the starrail module', description='Show game configuration in the starrail module')
show_config.set_defaults(func=entrypoint_handler.show_config)
parser.add_parser_to_group(game_info_group, show_config)
details_config = subparsers.add_parser('details', help='Show detailed information about the game and the client', description='Show detailed information about the game and the client')
details_config.set_defaults(func=entrypoint_handler.show_details)
parser.add_parser_to_group(game_info_group, details_config)
pt_config = subparsers.add_parser('runtime', help='Show time in hour, minutes, and seconds since this game session has started', description='Show time in hour, minutes, and seconds since this game session has started')
pt_config.set_defaults(func=entrypoint_handler.play_time)
parser.add_parser_to_group(game_info_group, pt_config)
# =================================================
# ===============| LAUNCH DRIVER | ================
# =================================================
apps_group = parser.add_group('Start/Stop Commands', 'Start/Stop or schedule Honkai: Star Rail application from the CLI.')
start_parser = subparsers.add_parser('start', help='Start the Honkai: Star Rail application', description='Start the Honkai: Star Rail application')
start_parser.set_defaults(func=entrypoint_handler.start)
parser.add_parser_to_group(apps_group, start_parser)
stop_parser = subparsers.add_parser('stop', help='Terminate the Honkai: Star Rail application', description='Terminate the Honkai: Star Rail application')
stop_parser.set_defaults(func=entrypoint_handler.stop)
parser.add_parser_to_group(apps_group, stop_parser)
schedule_parser = subparsers.add_parser('schedule', help='Schedule the start/stop of the application at a given time', description='Schedule the start/stop of the application at a given time')
schedule_parser.add_argument('subcommand', nargs='?', default=None, help='add / remove / show')
schedule_parser.add_argument('--action', type=str, default="start", help='Specify start or stop (only needed for schedule add)')
schedule_parser.add_argument('--time', type=str, help='Specify the scheduled time (only needed for schedule add) (i.e. 10:30)')
schedule_parser.set_defaults(func=entrypoint_handler.schedule)
parser.add_parser_to_group(apps_group, schedule_parser)
# =================================================
# =================| AUTOMATION | =================
# =================================================
auto_group = parser.add_group('Automation Commands', 'Simple automation for automating Honkai: Star Rail\'s gameplay.')
auto_parser = subparsers.add_parser('automation', help='', description='')
auto_parser.add_argument('action', nargs='?', default=None, help='record / run / show / remove / clear')
auto_parser.set_defaults(func=entrypoint_handler.automation)
parser.add_parser_to_group(auto_group, auto_parser)
click_parser = subparsers.add_parser('click', help='Continuously click mouse based on given interval.', description='Continuously click mouse based on given interval.')
click_parser.add_argument('--clicks', '-c', type=int, default=-1, help='Number of clicks. Leave empty (default) to run forever')
click_parser.add_argument('--interval', '-i', type=float, default=1, help='Interval delay (seconds) between clicks')
click_parser.add_argument('--randomize', '-r', type=float, default=1.0, help='Randomize the click interval by added 0 to x seconds to the interval specification.')
click_parser.add_argument('--hold', type=float, default=0.1, help='Delay (seconds) between click press and release')
click_parser.add_argument('--delay', '-d', type=float, default=3, help='Delay (seconds) before the clicks start')
click_parser.add_argument('--quiet', '-q', action='store_true', default=False, help='Run without verbosing progress')
click_parser.set_defaults(func=entrypoint_handler.click_continuously)
parser.add_parser_to_group(auto_group, click_parser)
# =================================================
# ==================| KEY URLS | ==================
# =================================================
url_group = parser.add_group('Official Pages', "Access official Honkai: Star Rail's web pages from the CLI")
homepage_parser = subparsers.add_parser('homepage', help='Open the official home page of Honkai: Star Rail', description='Open the official home page of Honkai: Star Rail')
homepage_parser.add_argument('-cn', action='store_true', default=False, help='Open CN version of the home page.')
homepage_parser.set_defaults(func=entrypoint_handler.homepage)
parser.add_parser_to_group(url_group, homepage_parser)
hoyolab_parser = subparsers.add_parser('hoyolab', help='Open the HoyoLab page of Honkai: Star Rail', description='Open the HoyoLab page of Honkai: Star Rail')
hoyolab_parser.set_defaults(func=entrypoint_handler.hoyolab)
parser.add_parser_to_group(url_group, hoyolab_parser)
youtube_parser = subparsers.add_parser('youtube', help='Open the official Youtube page of Honkai: Star Rail', description='Open the official Youtube page of Honkai: Star Rail')
youtube_parser.set_defaults(func=entrypoint_handler.youtube)
parser.add_parser_to_group(url_group, youtube_parser)
bilibili_parser = subparsers.add_parser('bilibili', help='Open the official BiliBili page of Honkai: Star Rail', description='Open the official BiliBili page of Honkai: Star Rail')
bilibili_parser.set_defaults(func=entrypoint_handler.bilibili)
parser.add_parser_to_group(url_group, bilibili_parser)
# =================================================
# =============| UTILITY FUNCTIONS | ==============
# =================================================
utility_group = parser.add_group('Utility Commands', 'Honkai: Star Rail utility features directly from the CLI.')
sc_config = subparsers.add_parser('screenshots', help='Open the screenshots directory in File Explorer', description='Open the screenshots directory in File Explorer')
sc_config.set_defaults(func=entrypoint_handler.screenshots)
parser.add_parser_to_group(utility_group, sc_config)
logs_config = subparsers.add_parser('game-logs', help='Open the games log directory in File Explorer', description='Open the games log directory in File Explorer')
logs_config.set_defaults(func=entrypoint_handler.game_logs)
parser.add_parser_to_group(utility_group, logs_config)
decode_config = subparsers.add_parser('decode', help='Decode ASCII-based binary files', description='Decode ASCII-based binary files')
decode_config.add_argument('--path', type=str, default=None, help='Path of the binary file to decode.')
decode_config.add_argument('--min-length', type=int, default=8, help='Minimum ASCII length to be considered as a valid string.')
decode_config.set_defaults(func=entrypoint_handler.decode)
parser.add_parser_to_group(utility_group, decode_config)
pulls_config = subparsers.add_parser('pulls', help='View the pull history page directly in the browser', description='View the pull history page directly in the browser')
pulls_config.set_defaults(func=entrypoint_handler.pulls)
parser.add_parser_to_group(utility_group, pulls_config)
cache_announcement_config = subparsers.add_parser('webcache', help='Show decoded web cache URLs (events, pulls, announcements)', description='Show decoded web cache URLs (events, pulls, announcements)')
cache_announcement_config.add_argument('--announcements', '-a', action='store_true', default=False, help='Show cached announcements.')
cache_announcement_config.add_argument('--events', '-e', action='store_true', default=False, help='Show cached events.')
cache_announcement_config.add_argument('--quiet', '-q', action='store_true', default=False, help='Do not verbose web cache explanation.')
cache_announcement_config.add_argument('--open', action='store_true', default=False, help='Open all web cache URLs found.')
cache_announcement_config.set_defaults(func=entrypoint_handler.webcache)
parser.add_parser_to_group(utility_group, cache_announcement_config)
# =================================================
# ==========| BASE CONFIGURATION CMD | ============
# =================================================
app_utility_group = parser.add_group('Configure Commands', 'Configure (auto-locate) and Honkai: Star Rail on the local machine. Must be ran before anything else.')
sync_parser = subparsers.add_parser('configure', help='Configure the starrail module on the local machine (auto-locate the Star Rail application). ', description="Configure the starrail module on the local machine (auto-locate the Star Rail application).")
sync_parser.set_defaults(func=entrypoint_handler.configure)
parser.add_parser_to_group(app_utility_group, sync_parser)
# =================================================
# ==============| HELPER FUNCTIONS | ==============
# =================================================
module_utility_group = parser.add_group('Helper Commands', '')
elev_parser = subparsers.add_parser('elevate', help=f'Request {COMMAND} to be ran as admin.', description=f'Request {COMMAND} to be ran as admin.')
elev_parser.add_argument('arguments', nargs='*', default=[], help=f'Any arguments to be followed after `{COMMAND}`')
elev_parser.set_defaults(func=entrypoint_handler.elevate)
parser.add_parser_to_group(module_utility_group, elev_parser)
# ================================================
# ==================| FIREFLY | ==================
# ================================================
ff_group = parser.add_group('???', 'What could this be?')
ff_parser = subparsers.add_parser('FIREFLY')
ff_parser.set_defaults(func=entrypoint_handler.firefly)
parser.add_parser_to_group(ff_group, ff_parser)
# ===========================================================================================
# >>> BLOCKING FUNCTIONS
# ===========================================================================================
if verify_platform() == False:
raise StarRailOSNotSupported()
module_fully_configured = entrypoint_handler.star_rail.config.full_configured()
if not module_fully_configured:
# If the module is not fully configured, then force user to first configure the module
def blocked_func(args):
text = color_cmd('starrail configure')
aprint(f"The StarRail module is not fully configured to run on this machine.\nTo configure the module, run `{text}`")
raise SRExit()
for name, subparser in subparsers.choices.items():
if name not in ["configure", "version", "author", "repo"]:
subparser.set_defaults(func=blocked_func)
# isadmin = is_admin()
# if not isadmin:
# # Certain commands require admin permissions to execute
# def blocked_func(args):
# elevate_cmd = color_cmd("amiya elevate", with_quotes=True)
# aprint(f"Insufficient permission. Run {elevate_cmd} to elevate permissions first.")
# raise SRExit()
# for name, subparser in subparsers.choices.items():
# if name in ["record-auto", "run-auto"]:
# subparser.set_defaults(func=blocked_func)
# ===========================================================================================
# >>> PARSER DRIVER
# ===========================================================================================
# Check if no command line arguments are provided
if len(sys.argv) == 1:
aprint("Loading starrail CLI environment...")
entrypoint_handler.start_cli(parser)
else:
# Normal command line execution
args = parser.parse_args()
if hasattr(args, 'func'):
try:
args.func(args)
except KeyboardInterrupt:
aprint("Keyboard Interrupt! StarRail Exiting.")
except SRExit:
exit()
else:
parser.print_help()
================================================
FILE: src/starrail/entrypoints/help_format_handler.py
================================================
import argparse
from starrail.utils.utils import *
class HelpFormatHandler:
def print_help(self, args, parser):
for group in parser.groups:
if group['description']:
title = Printer.to_lightred(u"\u2606 " + group['title'])
description = Printer.to_lightgrey(" : " + group['description'])
print(f"\n{title}{description}")
else:
title = Printer.to_lightred(u"\u2606 " + group['title'])
print(f"\n{title}")
for subparser in group['parsers']:
prog_cmd = Printer.to_lightblue(subparser.prog)
print(f" {prog_cmd}: {subparser.description or 'No description available.'}")
for action in subparser._actions:
if action.option_strings:
print(f" {Printer.to_purple(', '.join(action.option_strings))}: {Printer.to_lightgrey(action.help)}")
else:
print(f" {Printer.to_purple(action.dest)}: {Printer.to_lightgrey(action.help)}")
print("")
================================================
FILE: src/starrail/exceptions/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
================================================
FILE: src/starrail/exceptions/exceptions.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
class StarRailBaseException(Exception):
__module__ = 'builtins'
def __init__(self, message):
super().__init__(message)
class StarRailModuleException(Exception):
__module__ = 'builtins'
def __init__(self, err_code):
self.message = f"\nAn unexpected exception has occurred ({err_code}). Please seek help at https://github.com/ReZeroE/StarRail/issues."
super().__init__(self.message)
class StarRailOSNotSupported(Exception):
__module__ = 'builtins'
def __init__(self):
super().__init__(f"\nThe starrail package only supports Windows installations of Honkai Star Rail.")
class StarRailConfigNotExistsException(Exception):
__module__ = 'builtins'
def __init__(self, config_path):
super().__init__(f"\nThe StarRail config file cannot be found at {config_path} (should be caught accordingly).")
# This is a special exception that is used in place of exit() to avoid IO read error in the CLI environment (to avoid closing STDOUT).
# This error is caught and replaced with 'continue' in the CLI env and 'exit()' in the normal mode.
class SRExit(Exception):
__module__ = 'builtins'
def __init__(self, message="Module internal exit requested (should be caught accordingly)."):
super().__init__(message)
================================================
FILE: src/starrail/utils/__init__.py
================================================
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (c) 2024 Kevin L.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS O
gitextract_b305clb7/
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── pyproject.toml
├── requirements.txt
├── setup.cfg
├── setup.py
├── src/
│ └── starrail/
│ ├── __init__.py
│ ├── automation/
│ │ ├── __init__.py
│ │ ├── config/
│ │ │ ├── __init__.py
│ │ │ └── automation_config_handler.py
│ │ ├── pixel_calculator/
│ │ │ ├── __init__.py
│ │ │ ├── pixel_calculator.py
│ │ │ └── resolution_detector.py
│ │ ├── recorder.py
│ │ └── units/
│ │ ├── __init__.py
│ │ ├── action.py
│ │ └── sequence.py
│ ├── bin/
│ │ ├── __init__.py
│ │ ├── loader/
│ │ │ ├── __init__.py
│ │ │ └── loader.py
│ │ ├── logs/
│ │ │ └── starrail_log.txt
│ │ ├── pick/
│ │ │ ├── __init__.py
│ │ │ └── pick.py
│ │ ├── pid/
│ │ │ └── get_active_pid.c
│ │ └── scheduler/
│ │ ├── __init__.py
│ │ ├── config/
│ │ │ ├── __init__.py
│ │ │ └── starrail_schedule_config.py
│ │ └── starrail_scheduler.py
│ ├── config/
│ │ ├── __init__.py
│ │ └── config_handler.py
│ ├── constants.py
│ ├── controllers/
│ │ ├── __init__.py
│ │ ├── automation_controller.py
│ │ ├── c_click_controller.py
│ │ ├── star_rail_app.py
│ │ ├── streaming_assets_controller.py
│ │ ├── web_controller.py
│ │ └── webcache_controller.py
│ ├── data/
│ │ └── textfiles/
│ │ ├── disclaimer.txt
│ │ └── webcache_explain.txt
│ ├── entrypoints/
│ │ ├── __init__.py
│ │ ├── entrypoint_handler.py
│ │ ├── entrypoints.py
│ │ └── help_format_handler.py
│ ├── exceptions/
│ │ ├── __init__.py
│ │ └── exceptions.py
│ └── utils/
│ ├── __init__.py
│ ├── binary_decoder.py
│ ├── game_detector.py
│ ├── json_handler.py
│ ├── perm_elevate.py
│ ├── process_handler.py
│ └── utils.py
└── tests/
├── test_starrail.py
└── verify_package.py
SYMBOL INDEX (322 symbols across 29 files)
FILE: src/starrail/automation/config/automation_config_handler.py
class StarRailAutomationConfig (line 4) | class StarRailAutomationConfig:
method __init__ (line 5) | def __init__(self):
method load_all_automations (line 10) | def load_all_automations(self):
method load_automation (line 22) | def load_automation(self, automation_name):
method save_automation (line 26) | def save_automation(self, automation_name, payload):
method delete_automation (line 30) | def delete_automation(self, automation_name):
FILE: src/starrail/automation/pixel_calculator/pixel_calculator.py
class PixelCalculator (line 4) | class PixelCalculator:
method __init__ (line 5) | def __init__(self, monitor_info: dict):
method transform_coordinate (line 15) | def transform_coordinate(prev_coor: tuple, prev_window_info: dict):
FILE: src/starrail/automation/pixel_calculator/resolution_detector.py
class ResolutionDetector (line 12) | class ResolutionDetector:
method get_primary_monitor_size (line 16) | def get_primary_monitor_size() -> dict:
method get_foreground_window_size (line 28) | def get_foreground_window_size():
method get_window_size (line 40) | def get_window_size():
method get_window_info (line 63) | def get_window_info(pid, retry: int = 5):
FILE: src/starrail/automation/recorder.py
class AutomationRecorder (line 27) | class AutomationRecorder():
method __init__ (line 28) | def __init__(self, sequence: AutomationSequence, starrail_instance: Ho...
method create_indicator_window (line 41) | def create_indicator_window(self):
method stop_recording (line 103) | def stop_recording(self, root: tk.Tk):
method pause_or_resume_recording (line 109) | def pause_or_resume_recording(self):
method record (line 112) | def record(self, start_on_callback=False) -> AutomationSequence:
method __on_mouse_action (line 153) | def __on_mouse_action(self, x, y, button, pressed):
method __on_scroll_action (line 174) | def __on_scroll_action(self, x, y, dx, dy):
method __on_keyboard_action_press (line 191) | def __on_keyboard_action_press(self, key):
method __on_keyboard_action_release (line 206) | def __on_keyboard_action_release(self, key):
method on_mouse_action_update_window (line 243) | def on_mouse_action_update_window(self, x, y, delay):
method on_scroll_action_update_window (line 247) | def on_scroll_action_update_window(self, x, y, dx, dy, delay):
method on_keyboard_action_update_window (line 256) | def on_keyboard_action_update_window(self, key, delay, hold_time):
method on_pause_action_update_window (line 263) | def on_pause_action_update_window(self):
method __update_label (line 270) | def __update_label(self, text: str):
method __scale_window (line 274) | def __scale_window(self, standard_width, standard_height, standard_rad...
FILE: src/starrail/automation/units/action.py
class Action (line 16) | class Action(ABC):
method __init__ (line 18) | def __init__(self, *args):
method __repr__ (line 23) | def __repr__(self):
method execute (line 27) | def execute(self, *args):
method to_json (line 31) | def to_json(self):
class MouseAction (line 35) | class MouseAction(Action):
method __init__ (line 36) | def __init__(self, coor: tuple, delay: float, click: bool, window_info...
method execute (line 44) | def execute(self):
method to_json (line 61) | def to_json(self):
method __repr__ (line 72) | def __repr__(self):
method __is_valid_for_pixel_calc (line 75) | def __is_valid_for_pixel_calc(self):
class ScrollAction (line 87) | class ScrollAction(Action):
method __init__ (line 88) | def __init__(self, coor: tuple, dx: float, dy: float, delay: float, wi...
method execute (line 97) | def execute(self):
method to_json (line 107) | def to_json(self):
method __repr__ (line 121) | def __repr__(self):
method __is_valid_for_pixel_calc (line 124) | def __is_valid_for_pixel_calc(self):
class KeyboardAction (line 136) | class KeyboardAction(Action):
method __init__ (line 137) | def __init__(self, key: str, delay: float, hold_time: float):
method execute (line 142) | def execute(self, keyboard):
method to_json (line 150) | def to_json(self):
method __repr__ (line 157) | def __repr__(self):
method press_key (line 160) | def press_key(self, key: str, pynput_keyboard: pynput.keyboard.Control...
method reformat_key (line 182) | def reformat_key(self, key: keyboard.Key) -> str:
FILE: src/starrail/automation/units/sequence.py
class AutomationSequence (line 15) | class AutomationSequence:
method __init__ (line 16) | def __init__(
method __progress_bar (line 29) | def __progress_bar(self, actions: list, prefix="", size=40, out=sys.st...
method execute (line 67) | def execute(self):
method add (line 129) | def add(self, action: Action):
method to_json (line 133) | def to_json(self):
method parse_config (line 145) | def parse_config(raw_json_config: list):
method print_sequence (line 198) | def print_sequence(self):
method set_date_created_to_current (line 202) | def set_date_created_to_current(self):
method set_global_delay (line 205) | def set_global_delay(self, global_delay: int):
method get_runtime (line 208) | def get_runtime(self):
FILE: src/starrail/bin/loader/loader.py
class Loader (line 6) | class Loader:
method __init__ (line 7) | def __init__(self, desc="Loading...", end="Done!", timeout=0.1):
method start (line 24) | def start(self):
method _animate (line 28) | def _animate(self):
method __enter__ (line 35) | def __enter__(self):
method stop (line 38) | def stop(self):
method __exit__ (line 45) | def __exit__(self, exc_type, exc_value, tb):
FILE: src/starrail/bin/pick/pick.py
class Option (line 32) | class Option:
class Picker (line 50) | class Picker(Generic[OPTION_T]):
method __post_init__ (line 61) | def __post_init__(self) -> None:
method move_up (line 75) | def move_up(self) -> None:
method move_down (line 80) | def move_down(self) -> None:
method mark_index (line 85) | def mark_index(self) -> None:
method get_selected (line 92) | def get_selected(self) -> Union[List[PICK_RETURN_T], PICK_RETURN_T]:
method get_title_lines (line 104) | def get_title_lines(self) -> List[str]:
method get_option_lines (line 109) | def get_option_lines(self) -> List[str]:
method get_lines (line 136) | def get_lines(self) -> Tuple[List, int]:
method draw (line 143) | def draw(self, screen: "curses._CursesWindow") -> None:
method run_loop (line 166) | def run_loop(
method config_curses (line 186) | def config_curses(self) -> None:
method _start (line 196) | def _start(self, screen: "curses._CursesWindow"):
method start (line 200) | def start(self):
function pick (line 212) | def pick(
FILE: src/starrail/bin/pid/get_active_pid.c
function main (line 7) | int main() {
FILE: src/starrail/bin/scheduler/config/starrail_schedule_config.py
class StarRailScheduleConfig (line 4) | class StarRailScheduleConfig(JSONConfigHandler):
method __init__ (line 5) | def __init__(self):
method load_schedule (line 9) | def load_schedule(self):
method save_schedule (line 12) | def save_schedule(self, payload):
FILE: src/starrail/bin/scheduler/starrail_scheduler.py
class OperationTypes (line 15) | class OperationTypes(Enum):
class StartRailJob (line 20) | class StartRailJob:
method __init__ (line 21) | def __init__(self, job_id: int, job: schedule.Job, op_type: OperationT...
method __str__ (line 31) | def __str__(self):
method print_job (line 34) | def print_job(self):
method to_dict (line 37) | def to_dict(self):
class StarRailScheduler (line 48) | class StarRailScheduler:
method __init__ (line 49) | def __init__(self, starrail_instance: HonkaiStarRail):
method __load_schedules (line 65) | def __load_schedules(self):
method __run_scheduler (line 89) | def __run_scheduler(self):
method stop_scheduler (line 94) | def stop_scheduler(self):
method add_new_schedule (line 104) | def add_new_schedule(self, time_str, operation_type: OperationTypes):
method remove_schedule (line 133) | def remove_schedule(self):
method show_schedules (line 162) | def show_schedules(self):
method clear_schedules (line 173) | def clear_schedules(self):
method __parse_time_format (line 200) | def __parse_time_format(self, str_time: str):
method __parse_id (line 219) | def __parse_id(self, str_id: str):
method __get_job_with_id (line 227) | def __get_job_with_id(self, job_id: int):
method __get_next_job_id (line 235) | def __get_next_job_id(self):
FILE: src/starrail/config/config_handler.py
class StarRailConfig (line 34) | class StarRailConfig(JSONConfigHandler):
method __init__ (line 35) | def __init__(self):
method save_current_config (line 80) | def save_current_config(self):
method full_configured (line 95) | def full_configured(self) -> bool:
method path_configured (line 98) | def path_configured(self) -> bool:
method disclaimer_configured (line 111) | def disclaimer_configured(self) -> bool:
method set_path (line 119) | def set_path(self, game_path: str):
method __reset_config (line 125) | def __reset_config(self):
FILE: src/starrail/controllers/automation_controller.py
class StarRailAutomationController (line 14) | class StarRailAutomationController:
method __init__ (line 15) | def __init__(self, starrail_instance: HonkaiStarRail):
method __load_all_sequences (line 21) | def __load_all_sequences(self):
method verbose_general_usage (line 32) | def verbose_general_usage(self):
method get_all_sequences (line 50) | def get_all_sequences(self, list_format=False):
method get_sequence (line 55) | def get_sequence(self, sequence_name: str):
method get_sequence_with_id (line 62) | def get_sequence_with_id(self, sequence_id: int):
method run_requence (line 73) | def run_requence(self, sequence_name=None):
method show_sequences (line 113) | def show_sequences(self):
method record_sequences (line 123) | def record_sequences(self, sequence_name=None):
method delete_sequence (line 159) | def delete_sequence(self):
method clear_sequences (line 178) | def clear_sequences(self):
method tryget_int (line 199) | def tryget_int(self, user_input_id):
method __sequence_already_exist (line 210) | def __sequence_already_exist(self, r_sequence_name):
method get_range_string (line 216) | def get_range_string(self):
method get_next_sequence_id (line 221) | def get_next_sequence_id(self):
method __reformat_sequence_name (line 226) | def __reformat_sequence_name(self, sequence_name: str):
FILE: src/starrail/controllers/c_click_controller.py
class ContinuousClickController (line 11) | class ContinuousClickController:
method __init__ (line 12) | def __init__(self):
method click_continuously (line 18) | def click_continuously(
method __click (line 54) | def __click(self, hold_time, interval, randomize_interval):
method __verbose_start (line 65) | def __verbose_start(self, count, interval, randomize_by, hold_time, st...
method __verbose_click (line 92) | def __verbose_click(self, max_count):
method __on_press (line 99) | def __on_press(self, button):
FILE: src/starrail/controllers/star_rail_app.py
class HonkaiStarRail (line 21) | class HonkaiStarRail:
method __init__ (line 22) | def __init__(self):
method start (line 34) | def start(self) -> bool:
method terminate (line 54) | def terminate(self) -> bool:
method schedule (line 79) | def schedule(self):
method show_status (line 89) | def show_status(self, live=False):
method show_config (line 138) | def show_config(self):
method screenshots (line 154) | def screenshots(self):
method logs (line 165) | def logs(self):
method show_pulls (line 176) | def show_pulls(self):
method verbose_play_time (line 188) | def verbose_play_time(self):
method webcache_announcements (line 203) | def webcache_announcements(self):
method webcache_events (line 212) | def webcache_events(self):
method webcache_all (line 221) | def webcache_all(self):
method __print_cached_urls (line 244) | def __print_cached_urls(self, url_list):
method streaming_assets (line 253) | def streaming_assets(self):
method __get_screenshot_path (line 279) | def __get_screenshot_path(self):
method __get_log_path (line 282) | def __get_log_path(self):
method __get_game_config_path (line 285) | def __get_game_config_path(self):
method fetch_game_version (line 288) | def fetch_game_version(self):
method wait_to_start (line 310) | def wait_to_start(self, timeout = 30) -> bool:
method is_running (line 318) | def is_running(self) -> bool:
method is_focused (line 321) | def is_focused(self) -> bool:
method get_starrail_process (line 331) | def get_starrail_process(self) -> psutil.Process:
method proc_is_starrail (line 353) | def proc_is_starrail(self, starrail_proc: psutil.Process):
FILE: src/starrail/controllers/streaming_assets_controller.py
class StarRailStreamingAssetsBinaryFile (line 17) | class StarRailStreamingAssetsBinaryFile(Enum):
class StarRailStreamingAssetsController (line 23) | class StarRailStreamingAssetsController:
method __init__ (line 24) | def __init__(self, starrail_config: StarRailConfig):
method get_decoded_streaming_assets (line 32) | def get_decoded_streaming_assets(self, sa_binary_file: StarRailStreami...
method get_sa_binary_version (line 41) | def get_sa_binary_version(self):
method get_sa_client_config (line 44) | def get_sa_client_config(self):
method get_sa_dev_config (line 47) | def get_sa_dev_config(self):
method decode_streaming_assets (line 55) | def decode_streaming_assets(self, sa_binary_file: StarRailStreamingAss...
method parse_webcache (line 70) | def parse_webcache(self, decoded_strings, sa_binary_file: StarRailStre...
FILE: src/starrail/controllers/web_controller.py
class StarRailWebController (line 6) | class StarRailWebController:
method __init__ (line 7) | def __init__(self):
method homepage (line 10) | def homepage(self, cn=False):
method hoyolab (line 18) | def hoyolab(self):
method youtube (line 22) | def youtube(self):
method bilibili (line 26) | def bilibili(self):
FILE: src/starrail/controllers/webcache_controller.py
class StarRailWebCacheBinaryFile (line 19) | class StarRailWebCacheBinaryFile(Enum):
class StarRailWebCacheController (line 25) | class StarRailWebCacheController:
method __init__ (line 26) | def __init__(self, starrail_config: StarRailConfig):
method get_decoded_webcache (line 34) | def get_decoded_webcache(self, webcache_binary_file: StarRailWebCacheB...
method get_announcements_cache (line 42) | def get_announcements_cache(self):
method get_events_cache (line 45) | def get_events_cache(self):
method decode_webcache (line 53) | def decode_webcache(self, webcache_binary_file: StarRailWebCacheBinary...
method parse_webcache (line 84) | def parse_webcache(self, decoded_strings, webcache_file: StarRailWebCa...
method filter_urls (line 97) | def filter_urls(self, target_sequence: str, decoded_strings):
method should_ignore (line 109) | def should_ignore(self, url: str):
FILE: src/starrail/entrypoints/entrypoint_handler.py
class StarRailEntryPointHandler (line 24) | class StarRailEntryPointHandler:
method __init__ (line 25) | def __init__(self):
method about (line 35) | def about(self, args):
method version (line 48) | def version(self, args):
method author (line 58) | def author(self, args):
method repo (line 61) | def repo(self, args):
method show_status (line 71) | def show_status(self, args):
method show_config (line 74) | def show_config(self, args):
method show_details (line 77) | def show_details(self, args):
method play_time (line 80) | def play_time(self, args):
method start (line 87) | def start(self, args):
method stop (line 90) | def stop(self, args):
method schedule (line 93) | def schedule(self, args):
method automation (line 150) | def automation(self, args):
method click_continuously (line 183) | def click_continuously(self, args):
method homepage (line 209) | def homepage(self, args):
method hoyolab (line 212) | def hoyolab(self, args):
method youtube (line 215) | def youtube(self, args):
method bilibili (line 218) | def bilibili(self, args):
method screenshots (line 225) | def screenshots(self, args):
method game_logs (line 228) | def game_logs(self, args):
method decode (line 231) | def decode(self, args):
method pulls (line 235) | def pulls(self, args):
method webcache (line 238) | def webcache(self, args):
method configure (line 255) | def configure(self, args):
method elevate (line 307) | def elevate(self, args):
method print_title (line 315) | def print_title(self):
method print_init_help (line 338) | def print_init_help(self):
method clear_screen (line 358) | def clear_screen(self):
method check_custom_commands (line 361) | def check_custom_commands(self, user_input: str):
method setup_key_bindings (line 381) | def setup_key_bindings(self):
method start_cli (line 395) | def start_cli(self, parser: argparse.ArgumentParser):
method firefly (line 462) | def firefly(self, args):
FILE: src/starrail/entrypoints/entrypoints.py
class StarRailArgParser (line 14) | class StarRailArgParser(argparse.ArgumentParser):
method __init__ (line 15) | def __init__(self, *args, **kwargs):
method add_group (line 19) | def add_group(self, title, description=None):
method add_parser_to_group (line 24) | def add_parser_to_group(self, group, parser):
method error (line 27) | def error(self, message):
function start_starrail (line 45) | def start_starrail():
FILE: src/starrail/entrypoints/help_format_handler.py
class HelpFormatHandler (line 4) | class HelpFormatHandler:
method print_help (line 5) | def print_help(self, args, parser):
FILE: src/starrail/exceptions/exceptions.py
class StarRailBaseException (line 24) | class StarRailBaseException(Exception):
method __init__ (line 26) | def __init__(self, message):
class StarRailModuleException (line 29) | class StarRailModuleException(Exception):
method __init__ (line 31) | def __init__(self, err_code):
class StarRailOSNotSupported (line 35) | class StarRailOSNotSupported(Exception):
method __init__ (line 37) | def __init__(self):
class StarRailConfigNotExistsException (line 40) | class StarRailConfigNotExistsException(Exception):
method __init__ (line 42) | def __init__(self, config_path):
class SRExit (line 48) | class SRExit(Exception):
method __init__ (line 50) | def __init__(self, message="Module internal exit requested (should be ...
FILE: src/starrail/utils/binary_decoder.py
class StarRailBinaryDecoder (line 10) | class StarRailBinaryDecoder:
method __init__ (line 11) | def __init__(self):
method decode_raw_binary_file (line 14) | def decode_raw_binary_file(self, file_path, min_length=8):
method user_decode (line 35) | def user_decode(self, file_path=None, min_length=8):
FILE: src/starrail/utils/game_detector.py
class StarRailGameDetector (line 13) | class StarRailGameDetector:
method __init__ (line 17) | def __init__(self):
method get_local_drives (line 30) | def get_local_drives(self):
method find_game_in_path (line 35) | def find_game_in_path(self, path, name, stop_flag):
method find_game (line 52) | def find_game(self, paths=[], name=GAME_FILENAME):
method is_file_over_size (line 85) | def is_file_over_size(self, file_path):
FILE: src/starrail/utils/json_handler.py
class JSONConfigHandler (line 6) | class JSONConfigHandler(ABC):
method __init__ (line 7) | def __init__(self, config_abs_path, config_type=dict):
method LOAD_CONFIG (line 11) | def LOAD_CONFIG(self):
method SAVE_CONFIG (line 22) | def SAVE_CONFIG(self, json_payload) -> bool:
method DELETE_CONFIG (line 30) | def DELETE_CONFIG(self):
method CONFIG_EXISTS (line 39) | def CONFIG_EXISTS(self):
method VALIDATE_CONFIG (line 42) | def VALIDATE_CONFIG(self):
FILE: src/starrail/utils/perm_elevate.py
class StarRailPermissionsHandler (line 7) | class StarRailPermissionsHandler:
method elevate (line 9) | def elevate(arguments: list = []):
method elevate_post_cli (line 32) | def elevate_post_cli(follow_up_cmd: str):
FILE: src/starrail/utils/process_handler.py
class ProcessHandler (line 8) | class ProcessHandler:
method get_focused_pid (line 11) | def get_focused_pid():
method get_related_processes (line 22) | def get_related_processes(process_pid: int):
method kill_pid (line 40) | def kill_pid(pid, verbose_failure=True, is_child=False):
method kill_pid_and_residual (line 61) | def kill_pid_and_residual(pid):
FILE: src/starrail/utils/utils.py
class Printer (line 48) | class Printer:
method hex_text (line 50) | def hex_text(text, hex_color):
method to_purple (line 61) | def to_purple(text):
method to_lightpurple (line 65) | def to_lightpurple(text):
method to_skyblue (line 69) | def to_skyblue(text):
method to_lightgrey (line 73) | def to_lightgrey(text):
method to_blue (line 77) | def to_blue(text):
method to_lightblue (line 81) | def to_lightblue(text):
method to_darkblue (line 85) | def to_darkblue(text):
method to_lightgreen (line 89) | def to_lightgreen(text):
method to_lightred (line 93) | def to_lightred(text):
method to_githubblack (line 96) | def to_githubblack(text):
method to_pale_yellow (line 102) | def to_pale_yellow(text):
method to_light_blue (line 106) | def to_light_blue(text):
method to_teal (line 110) | def to_teal(text):
method to_turquoise (line 114) | def to_turquoise(text):
method to_dark_teal (line 118) | def to_dark_teal(text):
class LogType (line 124) | class LogType(Enum):
function __get_colored_prefix (line 130) | def __get_colored_prefix():
function __get_colored_submodule (line 134) | def __get_colored_submodule(submodule_name):
function atext (line 138) | def atext(text: str, log_type: LogType = LogType.NORMAL) -> str:
function aprint (line 142) | def aprint(
function get_prefix_space (line 170) | def get_prefix_space():
function color_cmd (line 173) | def color_cmd(text: str, with_quotes: bool = False):
function bool_to_str (line 188) | def bool_to_str(boolean: bool, true_text="Running", false_text="Not Runn...
class StarRailGameDetector (line 197) | class StarRailGameDetector:
method get_local_drives (line 201) | def get_local_drives(self):
method find_game_in_path (line 205) | def find_game_in_path(self, path, name, stop_flag):
method find_game (line 213) | def find_game(self, paths=[], name=GAME_FILENAME):
class StarRailScreenshotController (line 238) | class StarRailScreenshotController:
method __init__ (line 242) | def __init__(self):
method get_screenshot_path (line 245) | def get_screenshot_path(self):
method take_screenshot (line 248) | def take_screenshot(self):
function verify_platform (line 254) | def verify_platform():
function is_admin (line 265) | def is_admin() -> bool:
function print_disclaimer (line 281) | def print_disclaimer():
function print_webcache_explanation (line 287) | def print_webcache_explanation():
function center_text (line 300) | def center_text(text: str):
function print_centered (line 312) | def print_centered(text: str):
class DatetimeHandler (line 324) | class DatetimeHandler:
method get_datetime (line 326) | def get_datetime():
method get_datetime_str (line 329) | def get_datetime_str():
method get_time_str (line 332) | def get_time_str():
method datetime_to_str (line 336) | def datetime_to_str(datetime: datetime):
method str_to_datetime (line 340) | def str_to_datetime(datetime_str: str):
method epoch_to_time_str (line 344) | def epoch_to_time_str(epoch_time: float):
method epoch_to_datetime (line 348) | def epoch_to_datetime(epoch_time: float):
method seconds_to_time_str (line 352) | def seconds_to_time_str(seconds: int):
class HashCalculator (line 365) | class HashCalculator:
method SHA256 (line 367) | def SHA256(file_path: str):
function merge_dicts (line 376) | def merge_dicts(*dicts):
FILE: tests/verify_package.py
function check_init_files (line 7) | def check_init_files(build_dir=os.path.join(os.path.dirname(os.path.dirn...
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (231K chars).
[
{
"path": ".gitignore",
"chars": 3414,
"preview": "# Ignore existing configs\nsrc/starrail/config/starrail_config.json\nsrc/starrail/bin/scheduler/config/*.json\n\n# Local Scr"
},
{
"path": "CHANGELOG.md",
"chars": 5969,
"preview": "# Change Log\n\n### Version 1.0.5 - [9/25/2024]\n- Added feature to show live game status (`starrail status --live`).\n- Var"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3247,
"preview": "# Contributor Covenant Code of Conduct\n\n_Updated on 7/1/2024_\n\n## Our Pledge\n\nIn the interest of fostering an open and w"
},
{
"path": "LICENSE",
"chars": 1064,
"preview": "MIT License\n\nCopyright (c) 2024 Kevin L.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
},
{
"path": "MANIFEST.in",
"chars": 220,
"preview": "recursive-include src/starrail *.json *.png *.jpg *.jpeg *.txt *.c *.exe\n\nexclude src/starrail/config/*.json\nexclude src"
},
{
"path": "README.md",
"chars": 15036,
"preview": "# Honkai: Star Rail - `starrail`\n\n \n\n\n\n<p align=\"center\">\n <img src=\"https://i.imgur.com/lE9hrlV.png\" height=\"auto\" a"
},
{
"path": "pyproject.toml",
"chars": 195,
"preview": "[build-system]\nrequires = [\n \"setuptools>=42\",\n \"tabulate\",\n \"termcolor\",\n \"pytest\",\n \"psutil\",\n \"pynp"
},
{
"path": "requirements.txt",
"chars": 89,
"preview": "tabulate\ntermcolor\npytest\npsutil\npynput\npyautogui\npywin32\nscreeninfo\nschedule\npyreadline3"
},
{
"path": "setup.cfg",
"chars": 867,
"preview": "[metadata]\nname = starrail\nversion = 1.0.5\nauthor = Kevin L.\nauthor_email = kevinliu@vt.edu\ndescription = Honkai: Star R"
},
{
"path": "setup.py",
"chars": 1094,
"preview": "import os\nfrom setuptools import setup, find_packages\n\nsetup(\n name=\"starrail\",\n version=\"1.0.5\",\n author=\"Kevi"
},
{
"path": "src/starrail/__init__.py",
"chars": 1347,
"preview": "\"\"\"\nStarRail\n=====\nHonkai: Star Rail CLI Toolkit\n\"\"\"\n\n# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 202"
},
{
"path": "src/starrail/automation/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/automation/config/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/automation/config/automation_config_handler.py",
"chars": 1678,
"preview": "from starrail.utils.utils import *\nfrom starrail.utils.json_handler import JSONConfigHandler\n\nclass StarRailAutomationCo"
},
{
"path": "src/starrail/automation/pixel_calculator/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/automation/pixel_calculator/pixel_calculator.py",
"chars": 1669,
"preview": "from starrail.utils.utils import aprint, LogType\nfrom starrail.automation.pixel_calculator.resolution_detector import Re"
},
{
"path": "src/starrail/automation/pixel_calculator/resolution_detector.py",
"chars": 2845,
"preview": "import time\nimport win32process\nimport pygetwindow\nfrom ctypes import windll\nfrom screeninfo import get_monitors\nfrom wi"
},
{
"path": "src/starrail/automation/recorder.py",
"chars": 12242,
"preview": "import time\nimport threading\nimport tkinter as tk\nimport asyncio\nfrom pynput import mouse, keyboard\n\nfrom starrail.const"
},
{
"path": "src/starrail/automation/units/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/automation/units/action.py",
"chars": 6236,
"preview": "\nimport re\nimport time\nimport pyautogui\nimport pynput\nfrom pynput import keyboard\nfrom pynput.keyboard import Key\nfrom a"
},
{
"path": "src/starrail/automation/units/sequence.py",
"chars": 9546,
"preview": "import os\nimport time\nimport copy\nfrom datetime import datetime\nfrom pynput import keyboard\nfrom starrail.automation.uni"
},
{
"path": "src/starrail/bin/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/bin/loader/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/starrail/bin/loader/loader.py",
"chars": 1664,
"preview": "from itertools import cycle\nfrom shutil import get_terminal_size\nfrom threading import Thread\nfrom time import sleep\n\ncl"
},
{
"path": "src/starrail/bin/logs/starrail_log.txt",
"chars": 0,
"preview": ""
},
{
"path": "src/starrail/bin/pick/__init__.py",
"chars": 1111,
"preview": "# The MIT License (MIT)\n\n# Copyright (c) 2016 Wang Dàpéng\n\n# Permission is hereby granted, free of charge, to any person"
},
{
"path": "src/starrail/bin/pick/pick.py",
"chars": 7697,
"preview": "# The MIT License (MIT)\n\n# Copyright (c) 2016 Wang Dàpéng\n\n# Permission is hereby granted, free of charge, to any person"
},
{
"path": "src/starrail/bin/pid/get_active_pid.c",
"chars": 450,
"preview": "// Compile: \n// gcc .\\get_active_pid.c -o ../get_active_pid\n\n#include <windows.h>\n#include <stdio.h>\n\nint main() {\n "
},
{
"path": "src/starrail/bin/scheduler/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "src/starrail/bin/scheduler/config/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/bin/scheduler/config/starrail_schedule_config.py",
"chars": 473,
"preview": "from starrail.utils.utils import *\nfrom starrail.utils.json_handler import JSONConfigHandler\n\nclass StarRailScheduleConf"
},
{
"path": "src/starrail/bin/scheduler/starrail_scheduler.py",
"chars": 9131,
"preview": "import schedule\nimport time\nimport threading\nfrom enum import Enum\nimport re\nimport tabulate\n\nfrom starrail.utils.utils "
},
{
"path": "src/starrail/config/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/config/config_handler.py",
"chars": 5601,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/constants.py",
"chars": 5928,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/controllers/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/controllers/automation_controller.py",
"chars": 9871,
"preview": "\nimport os\nimport sys\nimport time\nimport tabulate\n\nfrom starrail.utils.utils import *\n\nfrom starrail.automation.units.se"
},
{
"path": "src/starrail/controllers/c_click_controller.py",
"chars": 4093,
"preview": "import time\nimport pyautogui\nimport random\nfrom pynput import keyboard\nfrom threading import Event\n\nfrom starrail.except"
},
{
"path": "src/starrail/controllers/star_rail_app.py",
"chars": 14168,
"preview": "\nimport os\nimport sys\nimport time\nimport psutil\nimport tabulate\nimport webbrowser\nimport subprocess\nimport configparser\n"
},
{
"path": "src/starrail/controllers/streaming_assets_controller.py",
"chars": 5632,
"preview": "import os\nimport re\nimport sys\nfrom pathlib import Path\n\nfrom enum import Enum\nfrom starrail.config.config_handler impor"
},
{
"path": "src/starrail/controllers/web_controller.py",
"chars": 1146,
"preview": "\nimport webbrowser\nfrom starrail.constants import HOMEPAGE_URL, HOMEPAGE_URL_CN, HOYOLAB_URL, YOUTUBE_URL, BILIBILI_URL\n"
},
{
"path": "src/starrail/controllers/webcache_controller.py",
"chars": 4531,
"preview": "\nimport os\nimport re\nimport sys\nimport shutil\nfrom pathlib import Path\n\nfrom enum import Enum\nfrom starrail.config.confi"
},
{
"path": "src/starrail/data/textfiles/disclaimer.txt",
"chars": 2294,
"preview": "=====================================================================\n================== | STARRAIL PACKAGE DISCLAIMER |"
},
{
"path": "src/starrail/data/textfiles/webcache_explain.txt",
"chars": 179,
"preview": "Web cache for Honkai: Star Rail stores recent web data. \nYou can open URLs to view announcements, events, or pull status"
},
{
"path": "src/starrail/entrypoints/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/entrypoints/entrypoint_handler.py",
"chars": 19363,
"preview": "import readline\nimport argparse\nimport getpass\nimport webbrowser\nimport tabulate\nimport subprocess\n\nfrom starrail.consta"
},
{
"path": "src/starrail/entrypoints/entrypoints.py",
"chars": 16838,
"preview": "import os\nimport sys\nimport time\nimport argparse\nstart_time = time.time()\n\nfrom starrail.entrypoints.entrypoint_handler "
},
{
"path": "src/starrail/entrypoints/help_format_handler.py",
"chars": 1143,
"preview": "import argparse\nfrom starrail.utils.utils import *\n\nclass HelpFormatHandler:\n def print_help(self, args, parser):\n "
},
{
"path": "src/starrail/exceptions/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/exceptions/exceptions.py",
"chars": 2445,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/utils/__init__.py",
"chars": 1133,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "src/starrail/utils/binary_decoder.py",
"chars": 1990,
"preview": "\n\nimport re\nimport os\nimport tabulate\nfrom starrail.utils.utils import *\n\nSUBMODULE_NAME = \"SR-DB\"\n\nclass StarRailBinary"
},
{
"path": "src/starrail/utils/game_detector.py",
"chars": 3372,
"preview": "import os\nimport sys\nimport string\nfrom pathlib import Path\nfrom concurrent import futures\nfrom multiprocessing import M"
},
{
"path": "src/starrail/utils/json_handler.py",
"chars": 1306,
"preview": "import os\nimport json\nfrom abc import ABC\nfrom starrail.exceptions.exceptions import StarRailBaseException\n\nclass JSONCo"
},
{
"path": "src/starrail/utils/perm_elevate.py",
"chars": 2037,
"preview": "import subprocess\nfrom starrail.utils.utils import *\nimport signal\nimport time\nimport psutil\n\nclass StarRailPermissionsH"
},
{
"path": "src/starrail/utils/process_handler.py",
"chars": 2565,
"preview": "import psutil\nimport time\nimport ctypes\nimport subprocess\n\nfrom starrail.utils.utils import *\n\nclass ProcessHandler:\n "
},
{
"path": "src/starrail/utils/utils.py",
"chars": 13024,
"preview": "# SPDX-License-Identifier: MIT\n# MIT License\n#\n# Copyright (c) 2024 Kevin L.\n#\n# Permission is hereby granted, free of c"
},
{
"path": "tests/test_starrail.py",
"chars": 1472,
"preview": "# # SPDX-License-Identifier: MIT\n# # MIT License\n# #\n# # Copyright (c) 2024 Kevin L.\n# #\n# # Permission is hereby grante"
},
{
"path": "tests/verify_package.py",
"chars": 639,
"preview": "import os\n\n'''\nInternal script to verify that all python directory is contains __init__.py\n'''\n\ndef check_init_files(bui"
}
]
About this extraction
This page contains the full source code of the ReZeroE/StarRail GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (214.2 KB), approximately 49.2k tokens, and a symbol index with 322 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.