.
\ No newline at end of file
diff --git a/README.md b/README.md
index 4fad2f5..f288f9b 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,10 @@
CS2 TriggerBot
Your ultimate aiming assistant for Counter-Strike 2
-
-
-
+[](https://github.com/Jesewe/cs2-triggerbot/releases)
+[](https://github.com/Jesewe/cs2-triggerbot/releases/latest/)
+[](LICENSE)
+[](https://boosty.to/jesewe)
Features •
Installation •
@@ -24,18 +25,23 @@ CS2 TriggerBot is an automated tool designed for Counter-Strike 2 that assists w
## Features
-- **Automatic Trigger**: Fires your weapon when an enemy is detected under your crosshair.
-- **Configurable Trigger Key**: Configure a keyboard key or mouse button (`x1` or `x2`) as the trigger via the GUI or `config.json` file.
-- **Configurable Delays**: Set minimum, maximum, and post-shot delays for more natural shooting behavior.
-- **Toggle Mode**: Enable this mode to keep the bot active after pressing the trigger key once, without holding it.
-- **Attack Teammates Option**: Toggle friendly fire with a checkbox in the GUI.
-- **Offsets and Client Data**: Automatically fetches the latest offsets and client data from remote sources.
-- **Logging**: Detailed logs are saved in `%LOCALAPPDATA%\Requests\ItsJesewe\crashes\tb_logs.log`.
-- **Update Checker**: Automatically checks for updates from the GitHub repository.
-- **GUI Interface**: Control the bot's behavior and configuration using the included graphical interface.
-- **Dynamic Config Update**: Automatically detects and applies changes to the `config.json` file without restarting the bot.
-- **Share/Import Settings**: Export your settings to share with others or import settings from a file.
-- **Reset to Defaults**: Restore all settings to their default values via the GUI.
+- **Automatic Trigger**: Fires when an enemy is detected under your crosshair.
+- **Configurable Trigger Key**: Set a keyboard key (e.g., `x`, `c`) or mouse button (`mouse4`, `mouse5`) via the GUI or `config.json`.
+- **Toggle Mode**: Activate the bot with a single key press instead of holding (configurable in the GUI).
+- **Configurable Delays**: Adjust minimum (`ShotDelayMin`), maximum (`ShotDelayMax`), and post-shot (`PostShotDelay`) delays for natural shooting behavior.
+- **Attack Teammates Option**: Toggle friendly fire via a checkbox in the GUI or `config.json`.
+- **Automatic Offset Fetching**: Retrieves the latest offsets and client data from remote sources on startup.
+- **Graphical User Interface (GUI)**:
+ - **Dashboard**: Monitor bot status, offset updates, and version info.
+ - **Settings Tab**: Configure trigger settings and delays with real-time validation.
+ - **Logs Tab**: View real-time logs from `%LOCALAPPDATA%\Requests\ItsJesewe\crashes\tb_logs.log`.
+ - **FAQ Tab**: Access answers to common questions.
+ - **Supporters Tab**: View a list of contributors and supporters who help the project.
+- **Dynamic Config Updates**: Detects and applies changes to `config.json` without restarting, thanks to `file_watcher.py`.
+- **Share/Import Settings**: Export settings as a compressed code or import from others via the GUI.
+- **Reset to Defaults**: Restore default settings with one click in the GUI.
+- **Update Checker**: Alerts you to new versions via GitHub releases.
+- **Logging**: Detailed logs saved to `%LOCALAPPDATA%\Requests\ItsJesewe\crashes\tb_logs.log` and a detailed version at `tb_detailed_logs.log`.
## Installation
@@ -86,23 +92,31 @@ Example `config.json`:
}
```
-- **TriggerKey**: The key or mouse button (`x`, `x1`, or `x2`) to activate the bot.
-- **ToggleMode**: If `true`, pressing the `TriggerKey` once activates the bot, and pressing it again deactivates it.
+### Configuration Options
+
+- **TriggerKey**: The key or mouse button (`x`, `mouse4`, or `mouse5`) to activate the bot.
+- **ToggleMode**: `true` enables toggle mode; `false` requires holding the key.
- **ShotDelayMin** and **ShotDelayMax**: Control the delay between shots to simulate natural behavior.
-- **PostShotDelay**: Set a delay after each shot for more controlled firing.
-- **AttackOnTeammates**: Set to `true` to enable friendly fire.
+- **PostShotDelay**: Delay after each shot (seconds).
+- **AttackOnTeammates**: `true` to enable friendly fire.
+
+**GUI Configuration:** Use the **Settings** tab to modify these values interactively. Changes are saved instantly and applied dynamically if `config.json` is edited externally.
## Usage
-1. Launch Counter-Strike 2.
-2. Run the TriggerBot using the command mentioned above or by launching the GUI version.
-3. Adjust settings like `Trigger Key`, `Toggle Mode`, `Shot Delay`, `Post Shot Delay`, `Attack Teammates`, or use `Share/Import Settings` and `Reset to Defaults` from the GUI.
-4. The bot will automatically start functioning when the game is active.
+1. **Launch Counter-Strike 2:** Ensure the game is running.
+2. **Start the Bot:** Run `main.py` or the executable, then click **Start Bot** in the Dashboard tab.
+3. **Configure Settings:** Adjust trigger key, delays, and other options in the **Settings** tab.
+4. **Monitor Activity:** Check the **Logs** tab for real-time updates or the **Dashboard** for status.
+5. **Toggle the Bot:** Use the configured trigger key to activate/deactivate (toggle mode) or hold to fire (hold mode).
+6. **Advanced Features:**
+ - **Share/Import:** Export/import settings via the Settings tab.
+ - **FAQ:** Refer to the FAQ tab for help.
## Troubleshooting
- **Failed to Fetch Offsets:** Ensure you have an active internet connection and that the source URLs are accessible.
-- **Errors with Offsets after Game Update:** After a Counter-Strike 2 game update, there may be issues with offsets, which can result in errors. In this case, please wait for updated offsets to be released.
+- **Errors with Offsets after Game Update:** After a Counter-Strike 2 game update, there may be issues with offsets, which can result in errors. Offsets are sourced from [https://github.com/a2x/cs2-dumper](https://github.com/a2x/cs2-dumper) and are not updated by the author of TriggerBot. Please wait for updated offsets to be released by the cs2-dumper repository.
- **Could Not Open `cs2.exe`:** Make sure the game is running and that you have the necessary permissions.
- **Unexpected Errors:** Check the log file located in the log directory for more details.
- **Issues with Importing Settings:** Ensure the imported config.json file is valid and matches the expected format.
@@ -111,6 +125,14 @@ Example `config.json`:
Contributions are welcome! Please open an issue or submit a pull request on the [GitHub repository](https://github.com/Jesewe/cs2-triggerbot).
+## Support the Developer
+
+If you find CS2 TriggerBot helpful and want to support its continued development, consider donating through Boosty. Your support helps maintain and improve this tool.
+
+- [Support on Boosty](https://boosty.to/jesewe)
+
+Thank you for your generosity!
+
## Disclaimer
This script is for educational purposes only. Using cheats or hacks in online games is against the terms of service of most games and can result in bans or other penalties. Use this script at your own risk.
diff --git a/classes/config_manager.py b/classes/config_manager.py
index 14bdc71..2a453fe 100644
--- a/classes/config_manager.py
+++ b/classes/config_manager.py
@@ -12,6 +12,10 @@ class ConfigManager:
Provides methods to load and save configuration settings,
with caching for efficiency and default configuration management.
"""
+ # Application version
+ VERSION = "v1.2.5"
+ # Directory where the update files are stored
+ UPDATE_DIRECTORY = os.path.expanduser(r'~\AppData\Local\Requests\ItsJesewe\Update')
# Directory where the configuration file is stored
CONFIG_DIRECTORY = os.path.expanduser(r'~\AppData\Local\Requests\ItsJesewe')
# Full path to the configuration file
@@ -38,8 +42,6 @@ def load_config(cls):
Loads the configuration from the configuration file.
- Creates the configuration directory and file with default settings if they do not exist.
- Caches the configuration to avoid redundant file reads.
- Returns:
- dict: The configuration settings.
"""
# Return cached configuration if available.
if cls._config_cache is not None:
@@ -90,9 +92,6 @@ def save_config(cls, config: dict, log_info: bool = True):
"""
Saves the configuration to the configuration file.
Updates the cache with the new configuration.
- Args:
- config (dict): The configuration settings to save.
- log_info (bool): Whether to log a success message after saving.
"""
cls._config_cache = config
try:
diff --git a/classes/logger.py b/classes/logger.py
index ec5c6cd..04d1b89 100644
--- a/classes/logger.py
+++ b/classes/logger.py
@@ -1,18 +1,23 @@
-import os, logging
+import os
+import logging
class Logger:
+ """
+ A class to handle logging for the application.
+ It sets up logging to a file, a detailed log file, and the console.
+ """
# Define the directory where logs will be stored.
- # Use an environment variable (%LOCALAPPDATA%) to ensure logs are stored in a user-specific location.
LOG_DIRECTORY = os.path.expanduser(r'~\AppData\Local\Requests\ItsJesewe\crashes')
-
# Define the full path for the log file within the LOG_DIRECTORY.
LOG_FILE = os.path.join(LOG_DIRECTORY, 'tb_logs.log')
-
# Define the full path for the detailed log file within the LOG_DIRECTORY.
DETAILED_LOG_FILE = os.path.join(LOG_DIRECTORY, 'tb_detailed_logs.log')
# Cache for the logger instance.
_logger = None
+
+ # Flag to prevent multiple logging setups
+ _logger_configured = False
@staticmethod
def setup_logging():
@@ -22,60 +27,66 @@ def setup_logging():
- Initializes the log files (clearing previous logs).
- Sets up logging to write messages to both a brief log file, a detailed log file, and the console.
"""
- # Ensure the log directory exists.
- os.makedirs(Logger.LOG_DIRECTORY, exist_ok=True)
-
- # Get the root logger and clear any existing handlers.
+ if Logger._logger_configured:
+ return
+ Logger._logger_configured = True
+
root_logger = logging.getLogger()
root_logger.handlers.clear()
- root_logger.setLevel(logging.INFO)
+ root_logger.setLevel(logging.DEBUG)
+
+ # Ensure log directory exists
+ try:
+ os.makedirs(Logger.LOG_DIRECTORY, exist_ok=True)
+ except Exception as e:
+ print(f"Error creating log directory {Logger.LOG_DIRECTORY}: {e}")
+ return # Exit setup if directory creation fails
- # Define the standard formatter for the brief logs and console.
+ # Standard formatter for brief logs and console
standard_formatter = logging.Formatter(
fmt='[%(asctime)s %(levelname)s]: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
- # Create a file handler for the brief log file.
- file_handler = logging.FileHandler(Logger.LOG_FILE, mode='w')
- file_handler.setLevel(logging.INFO)
- file_handler.setFormatter(standard_formatter)
- root_logger.addHandler(file_handler)
+ # File handler for tb_logs.log with error handling
+ try:
+ file_handler = logging.FileHandler(Logger.LOG_FILE, mode='w', encoding='utf-8')
+ file_handler.setLevel(logging.INFO)
+ file_handler.setFormatter(standard_formatter)
+ root_logger.addHandler(file_handler)
+ except Exception as e:
+ print(f"Error setting up file handler for {Logger.LOG_FILE}: {e}")
- # Create a stream handler for console output.
+ # Stream handler for console
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
stream_handler.setFormatter(standard_formatter)
root_logger.addHandler(stream_handler)
- # Define a detailed formatter.
+ # Detailed formatter and handler
detailed_formatter = logging.Formatter(
- fmt='[%(asctime)s %(levelname)s {%(module)s : %(funcName)s} (%(lineno)d)]: %(message)s',
+ fmt='[%(asctime)s.%(msecs)03d %(levelname)-8s] {%(name)s:%(module)s:%(funcName)s:%(lineno)d} [PID:%(process)d TID:%(thread)d]: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
+ try:
+ detailed_handler = logging.FileHandler(Logger.DETAILED_LOG_FILE, mode='w', encoding='utf-8')
+ detailed_handler.setLevel(logging.DEBUG)
+ detailed_handler.setFormatter(detailed_formatter)
+ root_logger.addHandler(detailed_handler)
+ except Exception as e:
+ print(f"Error setting up detailed handler for {Logger.DETAILED_LOG_FILE}: {e}")
- # Create a file handler for detailed logging.
- detailed_handler = logging.FileHandler(Logger.DETAILED_LOG_FILE, mode='w')
- detailed_handler.setLevel(logging.INFO)
- detailed_handler.setFormatter(detailed_formatter)
- root_logger.addHandler(detailed_handler)
+ # Test log to verify setup
+ logger = Logger.get_logger()
+ logger.info("Logging system initialized successfully.")
@staticmethod
def get_logger():
- """
- Provides a logger instance for use throughout the application.
- Returns:
- logging.Logger: A logger configured with the settings from `setup_logging`.
- """
if Logger._logger is None:
Logger._logger = logging.getLogger(__name__)
return Logger._logger
@staticmethod
def log_exception(exc: Exception):
- """
- Logs an exception along with its traceback.
- Use this method to capture exceptions in a standardized way.
- """
logger_instance = Logger.get_logger()
- logger_instance.error("An exception occurred", exc_info=True)
+ logger_instance.error("An exception occurred", exc_info=True)
\ No newline at end of file
diff --git a/classes/memory_manager.py b/classes/memory_manager.py
new file mode 100644
index 0000000..88fd53c
--- /dev/null
+++ b/classes/memory_manager.py
@@ -0,0 +1,120 @@
+import pymem
+import pymem.process
+from classes.logger import Logger
+from classes.utility import Utility
+
+# Initialize the logger for consistent logging
+logger = Logger.get_logger()
+
+class MemoryManager:
+ def __init__(self, offsets: dict, client_data: dict) -> None:
+ self.offsets, self.client_data = offsets, client_data
+ self.pm, self.client_base = None, None
+ self.ent_list = None # Cache for entity list pointer
+ # Offset attributes will be set by load_offsets
+ self.dwEntityList = None
+ self.dwLocalPlayerPawn = None
+ self.m_iHealth = None
+ self.m_iTeamNum = None
+ self.m_iIDEntIndex = None
+
+ def initialize(self) -> bool:
+ """
+ Initialize memory access by attaching to the process and setting up necessary data.
+ Returns True if successful, False otherwise.
+ """
+ # Check if pymem is initialized and the client module is retrieved
+ if not self.initialize_pymem() or not self.get_client_module():
+ return False
+ # Cache the entity list pointer
+ self.load_offsets()
+ if self.dwEntityList is None: # Ensure offsets were loaded successfully
+ return False
+ self.ent_list = self.pm.read_longlong(self.client_base + self.dwEntityList)
+ return True
+
+ def initialize_pymem(self) -> bool:
+ """Attach pymem to the game process."""
+ try:
+ # Attempt to attach to the cs2.exe process
+ self.pm = pymem.Pymem("cs2.exe")
+ logger.info("Successfully attached to cs2.exe process.")
+ return True
+ except pymem.exception.ProcessNotFound:
+ # Log an error if the process is not found
+ logger.error("cs2.exe process not found. Ensure the game is running.")
+ return False
+ except Exception as e:
+ # Log any other exceptions that may occur
+ logger.error(f"Unexpected error while attaching to cs2.exe: {e}")
+ return False
+
+ def get_client_module(self) -> bool:
+ """Retrieve the client.dll module base address."""
+ try:
+ # Attempt to retrieve the client.dll module
+ client_module = pymem.process.module_from_name(self.pm.process_handle, "client.dll")
+ self.client_base = client_module.lpBaseOfDll
+ logger.info("client.dll module found and base address retrieved.")
+ return True
+ except pymem.exception.ModuleNotFoundError:
+ # Log an error if the module is not found
+ logger.error("client.dll not found. Ensure it is loaded.")
+ return False
+ except Exception as e:
+ # Log any other exceptions that may occur
+ logger.error(f"Unexpected error while retrieving client.dll module: {e}")
+ return False
+
+ def load_offsets(self) -> None:
+ """Load memory offsets from Utility.extract_offsets."""
+ extracted = Utility.extract_offsets(self.offsets, self.client_data)
+ if extracted:
+ self.dwEntityList = extracted["dwEntityList"]
+ self.dwLocalPlayerPawn = extracted["dwLocalPlayerPawn"]
+ self.m_iHealth = extracted["m_iHealth"]
+ self.m_iTeamNum = extracted["m_iTeamNum"]
+ self.m_iIDEntIndex = extracted["m_iIDEntIndex"]
+ logger.info("Offsets initialized successfully.")
+ else:
+ logger.error("Failed to initialize offsets from extracted data.")
+
+ def get_entity(self, index: int):
+ """Retrieve an entity from the entity list."""
+ try:
+ # Use cached entity list pointer
+ list_offset = 0x8 * (index >> 9)
+ ent_entry = self.pm.read_longlong(self.ent_list + list_offset + 0x10)
+ entity_offset = 120 * (index & 0x1FF)
+ return self.pm.read_longlong(ent_entry + entity_offset)
+ except Exception as e:
+ logger.error(f"Error reading entity: {e}")
+ return None
+
+ def get_fire_logic_data(self) -> dict | None:
+ """Retrieve data necessary for firing logic."""
+ try:
+ # Read the local player and entity ID
+ player = self.pm.read_longlong(self.client_base + self.dwLocalPlayerPawn)
+ entity_id = self.pm.read_int(player + self.m_iIDEntIndex)
+
+ if entity_id > 0:
+ # Retrieve the entity, team, and health
+ entity = self.get_entity(entity_id)
+ if entity:
+ entity_team = self.pm.read_int(entity + self.m_iTeamNum)
+ player_team = self.pm.read_int(player + self.m_iTeamNum)
+ entity_health = self.pm.read_int(entity + self.m_iHealth)
+ return {
+ "entity_team": entity_team,
+ "player_team": player_team,
+ "entity_health": entity_health
+ }
+ return None
+ except Exception as e:
+ # Log any exceptions that may occur during the process
+ if "Could not read memory at" in str(e):
+ logger.error("Game was updated, new offsets are required. Please wait for the offsets update.")
+ else:
+ logger.error(f"Error in fire logic: {e}")
+ return None
\ No newline at end of file
diff --git a/classes/trigger_bot.py b/classes/trigger_bot.py
index 93f7c51..4b97e3f 100644
--- a/classes/trigger_bot.py
+++ b/classes/trigger_bot.py
@@ -1,9 +1,10 @@
-import threading, time, random, pymem, pymem.process, keyboard, winsound
+import threading, time, random, keyboard, winsound
from pynput.mouse import Controller, Button, Listener as MouseListener
from pynput.keyboard import Listener as KeyboardListener
from classes.config_manager import ConfigManager
+from classes.memory_manager import MemoryManager
from classes.logger import Logger
from classes.utility import Utility
@@ -15,25 +16,20 @@
MAIN_LOOP_SLEEP = 0.05
class CS2TriggerBot:
- VERSION = "v1.2.4.5"
-
def __init__(self, offsets: dict, client_data: dict) -> None:
"""
Initialize the TriggerBot with offsets, configuration, and client data.
"""
# Load the configuration settings
self.config = ConfigManager.load_config()
- self.offsets, self.client_data = offsets, client_data
- self.pm, self.client_base = None, None
+ self.memory_manager = MemoryManager(offsets, client_data)
self.is_running, self.stop_event = False, threading.Event()
self.trigger_active = False
self.toggle_state = False
- self.ent_list = None # Cache for entity list pointer
self.update_config(self.config)
- # Initialize offsets and configuration settings
+ # Initialize configuration settings
self.load_configuration()
- self.initialize_offsets()
# Setup listeners
self.keyboard_listener = KeyboardListener(on_press=self.on_key_press, on_release=self.on_key_release)
@@ -50,23 +46,16 @@ def load_configuration(self) -> None:
self.shot_delay_max = settings['ShotDelayMax']
self.post_shot_delay = settings['PostShotDelay']
self.attack_on_teammates = settings['AttackOnTeammates']
- # Check if the trigger key is a mouse button
- self.is_mouse_trigger = self.trigger_key in ["x1", "x2"]
+
+ # Determine if the trigger key is a mouse button
+ self.mouse_button_map = {
+ "mouse3": Button.middle,
+ "mouse4": Button.x1,
+ "mouse5": Button.x2,
+ }
- def initialize_offsets(self) -> None:
- """Load memory offsets."""
- try:
- client = self.offsets["client.dll"]
- self.dwEntityList = client["dwEntityList"]
- self.dwLocalPlayerPawn = client["dwLocalPlayerPawn"]
-
- classes = self.client_data["client.dll"]["classes"]
- self.m_iHealth = classes["C_BaseEntity"]["fields"]["m_iHealth"]
- self.m_iTeamNum = classes["C_BaseEntity"]["fields"]["m_iTeamNum"]
- self.m_iIDEntIndex = classes["C_CSPlayerPawnBase"]["fields"]["m_iIDEntIndex"]
- logger.info("Offsets have been initialized.")
- except KeyError as e:
- logger.error(f"Offset initialization error: Missing key {e}")
+ # Check if the trigger key is a mouse button
+ self.is_mouse_trigger = self.trigger_key in self.mouse_button_map
def update_config(self, config):
"""Update the configuration settings."""
@@ -110,92 +99,25 @@ def on_key_release(self, key) -> None:
def on_mouse_click(self, x, y, button, pressed) -> None:
"""Handle mouse click events."""
- if self.is_mouse_trigger and button == Button[self.trigger_key]:
+ if not self.is_mouse_trigger:
+ return
+
+ expected_btn = self.mouse_button_map.get(self.trigger_key)
+ if button == expected_btn:
if self.toggle_mode and pressed:
self.toggle_state = not self.toggle_state
self.play_toggle_sound(self.toggle_state)
else:
self.trigger_active = pressed
- def initialize_pymem(self) -> bool:
- """Attach pymem to the game process."""
- try:
- # Attempt to attach to the cs2.exe process
- self.pm = pymem.Pymem("cs2.exe")
- logger.info("Successfully attached to cs2.exe process.")
- return True
- except pymem.exception.ProcessNotFound:
- # Log an error if the process is not found
- logger.error("cs2.exe process not found. Ensure the game is running.")
- return False
- except Exception as e:
- # Log any other exceptions that may occur
- logger.error(f"Unexpected error while attaching to cs2.exe: {e}")
- return False
-
- def get_client_module(self) -> bool:
- """Retrieve the client.dll module base address."""
- try:
- # Attempt to retrieve the client.dll module
- client_module = pymem.process.module_from_name(self.pm.process_handle, "client.dll")
- self.client_base = client_module.lpBaseOfDll
- logger.info("client.dll module found and base address retrieved.")
- return True
- except pymem.exception.ModuleNotFoundError:
- # Log an error if the module is not found
- logger.error("client.dll not found. Ensure it is loaded.")
- return False
- except Exception as e:
- # Log any other exceptions that may occur
- logger.error(f"Unexpected error while retrieving client.dll module: {e}")
- return False
-
- def get_entity(self, index: int):
- """Retrieve an entity from the entity list."""
- try:
- # Use cached entity list pointer
- list_offset = 0x8 * (index >> 9)
- ent_entry = self.pm.read_longlong(self.ent_list + list_offset + 0x10)
- entity_offset = 120 * (index & 0x1FF)
- return self.pm.read_longlong(ent_entry + entity_offset)
- except Exception as e:
- logger.error(f"Error reading entity: {e}")
- return None
-
def should_trigger(self, entity_team: int, player_team: int, entity_health: int) -> bool:
"""Determine if the bot should fire."""
return (self.attack_on_teammates or entity_team != player_team) and entity_health > 0
- def perform_fire_logic(self) -> None:
- """Execute the firing logic."""
- try:
- # Read the local player and entity ID
- player = self.pm.read_longlong(self.client_base + self.dwLocalPlayerPawn)
- entity_id = self.pm.read_int(player + self.m_iIDEntIndex)
-
- if entity_id > 0:
- # Retrieve the entity, team, and health
- entity = self.get_entity(entity_id)
- if entity:
- entity_team = self.pm.read_int(entity + self.m_iTeamNum)
- player_team = self.pm.read_int(player + self.m_iTeamNum)
- entity_health = self.pm.read_int(entity + self.m_iHealth)
- # Check if the bot should fire
- if self.should_trigger(entity_team, player_team, entity_health):
- time.sleep(random.uniform(self.shot_delay_min, self.shot_delay_max))
- mouse.click(Button.left)
- time.sleep(self.post_shot_delay)
- except Exception as e:
- # Log any exceptions that may occur during the process
- logger.error(f"Error in fire logic: {e}")
-
def start(self) -> None:
"""Start the TriggerBot."""
- # Check if pymem is initialized and the client module is retrieved
- if not self.initialize_pymem() or not self.get_client_module():
+ if not self.memory_manager.initialize():
return
- # Cache the entity list pointer
- self.ent_list = self.pm.read_longlong(self.client_base + self.dwEntityList)
# Set the running flag to True and log that the TriggerBot has started
self.is_running = True
logger.info("TriggerBot started.")
@@ -213,12 +135,24 @@ def start(self) -> None:
if self.toggle_mode:
if self.toggle_state:
- self.perform_fire_logic()
+ data = self.memory_manager.get_fire_logic_data()
+ if data and self.should_trigger(data["entity_team"], data["player_team"], data["entity_health"]):
+ sleep(random.uniform(self.shot_delay_min, self.shot_delay_max))
+ mouse.click(Button.left)
+ sleep(self.post_shot_delay)
else:
if self.is_mouse_trigger and self.trigger_active:
- self.perform_fire_logic()
+ data = self.memory_manager.get_fire_logic_data()
+ if data and self.should_trigger(data["entity_team"], data["player_team"], data["entity_health"]):
+ sleep(random.uniform(self.shot_delay_min, self.shot_delay_max))
+ mouse.click(Button.left)
+ sleep(self.post_shot_delay)
elif not self.is_mouse_trigger and keyboard.is_pressed(self.trigger_key):
- self.perform_fire_logic()
+ data = self.memory_manager.get_fire_logic_data()
+ if data and self.should_trigger(data["entity_team"], data["player_team"], data["entity_health"]):
+ sleep(random.uniform(self.shot_delay_min, self.shot_delay_max))
+ mouse.click(Button.left)
+ sleep(self.post_shot_delay)
sleep(MAIN_LOOP_SLEEP)
except KeyboardInterrupt:
diff --git a/classes/utility.py b/classes/utility.py
index 440a91d..a10db4d 100644
--- a/classes/utility.py
+++ b/classes/utility.py
@@ -8,9 +8,6 @@
from packaging import version
from dateutil.parser import parse as parse_date
-from PyQt6.QtWidgets import QDialog, QProgressBar, QMessageBox, QVBoxLayout
-from PyQt6.QtCore import QThread, pyqtSignal
-
from classes.logger import Logger
# Initialize the logger for consistent logging
@@ -63,18 +60,21 @@ def fetch_offsets():
@staticmethod
def check_for_updates(current_version):
- """
- Checks GitHub for the latest version using orjson for JSON parsing.
- """
+ """Checks GitHub for the latest version and returns the download URL of 'CS2.Triggerbot.exe' if an update is available."""
try:
response = requests.get("https://api.github.com/repos/Jesewe/cs2-triggerbot/releases/latest")
response.raise_for_status()
data = orjson.loads(response.content)
latest_version = data.get("tag_name")
- update_url = data.get("html_url")
if version.parse(latest_version) > version.parse(current_version):
- logger.info(f"New version available: {latest_version}.")
- return update_url
+ for asset in data.get("assets", []):
+ if asset.get("name") == "CS2.Triggerbot.exe":
+ download_url = asset.get("browser_download_url")
+ if download_url:
+ logger.info(f"New version available: {latest_version}.")
+ return download_url
+ logger.warning("No 'CS2.Triggerbot.exe' found in the latest release assets.")
+ return None
logger.info("No new updates available.")
return None
except requests.exceptions.RequestException as e:
@@ -84,54 +84,6 @@ def check_for_updates(current_version):
logger.error(f"An unexpected error occurred during update check: {e}")
return None
- @staticmethod
- def get_latest_exe_download_url():
- """
- Retrieves the direct download URL for the 'CS2.Triggerbot.exe' asset
- from the latest GitHub release, parsing JSON with orjson.
- """
- try:
- response = requests.get("https://api.github.com/repos/Jesewe/cs2-triggerbot/releases/latest")
- response.raise_for_status()
- data = orjson.loads(response.content)
- for asset in data.get("assets", []):
- if asset.get("name") == "CS2.Triggerbot.exe":
- return asset.get("browser_download_url")
- logger.error("Executable asset not found in the latest release.")
- return None
- except Exception as e:
- logger.error(f"Error getting update asset URL: {e}")
- return None
-
- @staticmethod
- def fetch_last_offset_update(last_update_label):
- """
- Fetches the timestamp of the latest commit to the offsets repository.
- """
- try:
- response = requests.get("https://api.github.com/repos/a2x/cs2-dumper/commits/main")
- response.raise_for_status()
- commit_data = orjson.loads(response.content)
- commit_timestamp = commit_data["commit"]["committer"]["date"]
-
- last_update_dt = parse_date(commit_timestamp)
- formatted_timestamp = last_update_dt.strftime("%m/%d/%Y %H:%M:%S")
- last_update_label.setText(f"Last offsets update: {formatted_timestamp} (UTC)")
- last_update_label.setStyleSheet("font-size: 16px; color: #ffa420; font-weight: bold;")
- except requests.exceptions.HTTPError as e:
- if response.status_code == 403:
- last_update_label.setText("Request limit exceeded. Please try again later.")
- last_update_label.setStyleSheet("font-size: 16px; color: #0bda51; font-weight: bold;")
- logger.error(f"Offset update fetch failed: {e} (403 Forbidden)")
- else:
- last_update_label.setText("Error fetching last offsets update. Please check your internet connection or try again later.")
- last_update_label.setStyleSheet("font-size: 16px; color: #0bda51; font-weight: bold;")
- logger.error(f"Offset update fetch failed: {e}")
- except Exception as e:
- last_update_label.setText("Error fetching last offsets update. Please check your internet connection or try again later.")
- last_update_label.setStyleSheet("font-size: 16px; color: #0bda51; font-weight: bold;")
- logger.error(f"Offset update fetch failed: {e}")
-
@staticmethod
def resource_path(relative_path):
"""Returns the path to a resource, supporting both normal startup and frozen .exe."""
@@ -152,4 +104,27 @@ def is_game_active():
@staticmethod
def is_game_running():
"""Check if the game process is running using psutil."""
- return any(proc.info['name'] == 'cs2.exe' for proc in psutil.process_iter(attrs=['name']))
\ No newline at end of file
+ return any(proc.info['name'] == 'cs2.exe' for proc in psutil.process_iter(attrs=['name']))
+
+ @staticmethod
+ def extract_offsets(offsets: dict, client_data: dict) -> dict | None:
+ """Load memory offsets."""
+ try:
+ client = offsets["client.dll"]
+ dwEntityList = client["dwEntityList"]
+ dwLocalPlayerPawn = client["dwLocalPlayerPawn"]
+
+ classes = client_data["client.dll"]["classes"]
+ m_iHealth = classes["C_BaseEntity"]["fields"]["m_iHealth"]
+ m_iTeamNum = classes["C_BaseEntity"]["fields"]["m_iTeamNum"]
+ m_iIDEntIndex = classes["C_CSPlayerPawnBase"]["fields"]["m_iIDEntIndex"]
+ return {
+ "dwEntityList": dwEntityList,
+ "dwLocalPlayerPawn": dwLocalPlayerPawn,
+ "m_iHealth": m_iHealth,
+ "m_iTeamNum": m_iTeamNum,
+ "m_iIDEntIndex": m_iIDEntIndex
+ }
+ except KeyError as e:
+ logger.error(f"Offset initialization error: Missing key {e}")
+ return None
\ No newline at end of file
diff --git a/gui/faq_tab.py b/gui/faq_tab.py
index 4f07a5f..65b1ad9 100644
--- a/gui/faq_tab.py
+++ b/gui/faq_tab.py
@@ -1,35 +1,129 @@
-from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextEdit
+import customtkinter as ctk
-def init_faq_tab(main_window):
- """
- Sets up the FAQs tab with common questions and answers.
- """
- faq_tab = QWidget()
- layout = QVBoxLayout()
- faqs_content = """
- Frequently Asked Questions
- Q: What is a TriggerBot?
- A: A TriggerBot is a tool that automatically shoots when your crosshair is over an enemy.
- Q: Is this tool safe to use?
- A: This tool is for educational purposes only. Use it at your own risk.
- Q: How do I start the TriggerBot?
- A: Go to the 'Home' tab and click 'Start Bot' after ensuring the game is running.
- Q: How can I update the offsets?
- A: Offsets are fetched automatically from the server. Check the 'Home' tab for the last update timestamp.
- Q: Can I customize the bot's behavior?
- A: Yes, use the 'General Settings' tab to adjust key configurations, delays, and teammate attack settings.
- Q: Does the TriggerBot work on FACEIT?
- A: No, using the TriggerBot on FACEIT is against their terms of service and can result in a ban.
- Q: I found a bug, where can I report it?
- A: Report bugs by opening an issue on our GitHub Issues page.
- Q: How can I contribute to the project?
- A: Open a pull request on our GitHub Pull Requests page.
- Q: Where can I join the community?
- A: Join our Telegram Channel for updates and support.
- """
- faqs_widget = QTextEdit()
- faqs_widget.setHtml(faqs_content)
- faqs_widget.setReadOnly(True)
- layout.addWidget(faqs_widget)
- faq_tab.setLayout(layout)
- main_window.tabs.addTab(faq_tab, "FAQs")
\ No newline at end of file
+def populate_faq(main_window, frame):
+ """Populate the FAQ frame with questions and answers."""
+ # Scrollable container for FAQ content
+ faq_container = ctk.CTkScrollableFrame(
+ frame,
+ fg_color="transparent"
+ )
+ faq_container.pack(fill="both", expand=True, padx=40, pady=40)
+
+ # Frame for page title and subtitle
+ title_frame = ctk.CTkFrame(faq_container, fg_color="transparent")
+ title_frame.pack(fill="x", pady=(0, 40))
+
+ # FAQ title with icon
+ ctk.CTkLabel(
+ title_frame,
+ text="❓ Frequently Asked Questions",
+ font=("Chivo", 32, "bold"),
+ text_color=("#1f2937", "#E0E0E0")
+ ).pack(anchor="w")
+
+ # Subtitle providing context
+ ctk.CTkLabel(
+ title_frame,
+ text="Find answers to common questions about TriggerBot usage and configuration",
+ font=("Gambetta", 16),
+ text_color=("#6b7280", "#9ca3af")
+ ).pack(anchor="w", pady=(8, 0))
+
+ # List of FAQ items
+ faqs = [
+ ("What is a TriggerBot?", "A TriggerBot automatically shoots when your crosshair is positioned over an enemy player, providing enhanced reaction times in competitive gameplay. It works by detecting enemy pixels and triggering mouse clicks."),
+ ("Is this tool safe to use?", "This tool is provided for educational and research purposes only. Using automation tools in online games may violate terms of service and could result in account penalties. Always check game rules before use."),
+ ("How do I configure the trigger key?", "Navigate to Settings and enter your preferred key in the Trigger Key field. You can use keyboard keys (e.g., 'x', 'c', 'v') or mouse buttons (e.g., 'mouse4' for mouse button 4, 'mouse5' for mouse button 5)."),
+ ("What are the delay settings for?", "Delay settings give the bot a more natural feel by adding timing differences. You can set minimum and maximum delays to randomize how quickly it shoots. Plus, the Post Shot Delay adds a short pause after each shot, making it seem like a real person is reacting."),
+ ("Can I use this on FACEIT or ESEA?", "No, you can't use automation tools on anti-cheat platforms like FACEIT, ESEA, or VAC-secured servers. Doing so will get you a permanent ban. Stick to casual servers or practice offline instead."),
+ ("How do I update the offsets?", "When you start the app, it automatically gets the latest offsets from the server. You can see when it was last updated on the dashboard. If you want to refresh it manually, just go to Settings."),
+ ("Why isn't the bot triggering?", "Here are some usual problems: the trigger key might be set wrong, the game window isn't focused, you might not see the enemy in your crosshair, or your game settings might have changed. Take a look at your Settings to make sure everything's set up right."),
+ ("What should I do if the app crashes?", "First, try restarting the app. If it's still crashing, make sure you have the latest version, check that your system meets the requirements, and see if any antivirus is blocking the app."),
+ ("Is there a hotkey to toggle the bot on/off?", "Yes, you can set a toggle hotkey in Settings. This allows you to quickly enable/disable the triggerbot during gameplay without alt-tabbing to the application.")
+ ]
+
+ # Create FAQ cards
+ for i, (question, answer) in enumerate(faqs):
+ # Card for each FAQ item
+ faq_card = ctk.CTkFrame(
+ faq_container,
+ corner_radius=12,
+ fg_color=("#ffffff", "#161b22"),
+ border_width=1,
+ border_color=("#e5e7eb", "#30363d")
+ )
+ faq_card.pack(fill="x", pady=(0, 16))
+
+ # Frame for question header
+ question_frame = ctk.CTkFrame(faq_card, fg_color="transparent")
+ question_frame.pack(fill="x", padx=24, pady=(20, 10))
+
+ # Number badge for question
+ number_badge = ctk.CTkFrame(
+ question_frame,
+ width=30,
+ height=30,
+ corner_radius=15,
+ fg_color="#D5006D"
+ )
+ number_badge.pack(side="left", padx=(0, 12))
+ number_badge.pack_propagate(False)
+
+ # Number inside badge
+ ctk.CTkLabel(
+ number_badge,
+ text=str(i+1),
+ font=("Chivo", 12, "bold"),
+ text_color="white"
+ ).place(relx=0.5, rely=0.5, anchor="center")
+
+ # Question text
+ question_label = ctk.CTkLabel(
+ question_frame,
+ text=question,
+ font=("Chivo", 16, "bold"),
+ text_color=("#1f2937", "#E0E0E0"),
+ anchor="w"
+ )
+ question_label.pack(side="left", fill="x", expand=True)
+
+ # Frame for answer text
+ answer_frame = ctk.CTkFrame(faq_card, fg_color="transparent")
+ answer_frame.pack(fill="x", padx=66, pady=(0, 20))
+
+ # Answer text with wrapping
+ ctk.CTkLabel(
+ answer_frame,
+ text=answer,
+ font=("Gambetta", 14),
+ text_color=("#4b5563", "#9ca3af"),
+ anchor="w",
+ wraplength=750,
+ justify="left"
+ ).pack(fill="x")
+
+ # Footer with additional help information
+ footer_frame = ctk.CTkFrame(
+ faq_container,
+ corner_radius=12,
+ fg_color=("#f8fafc", "#0d1117"),
+ border_width=1,
+ border_color=("#e2e8f0", "#21262d")
+ )
+ footer_frame.pack(fill="x", pady=(30, 0))
+
+ # Footer title
+ ctk.CTkLabel(
+ footer_frame,
+ text="💡 Still have questions?",
+ font=("Chivo", 16, "bold"),
+ text_color=("#1f2937", "#E0E0E0")
+ ).pack(pady=(20, 5))
+
+ # Footer guidance
+ ctk.CTkLabel(
+ footer_frame,
+ text="Check the documentation or visit github issues for additional support and tips.",
+ font=("Gambetta", 14),
+ text_color=("#6b7280", "#9ca3af")
+ ).pack(pady=(0, 20))
\ No newline at end of file
diff --git a/gui/general_settings_tab.py b/gui/general_settings_tab.py
index 72a1860..af5704a 100644
--- a/gui/general_settings_tab.py
+++ b/gui/general_settings_tab.py
@@ -1,215 +1,372 @@
-import orjson, base64, zlib
+import customtkinter as ctk
-from PyQt6.QtWidgets import QWidget, QFormLayout, QLineEdit, QCheckBox, QPushButton, QHBoxLayout, QSpacerItem, QSizePolicy, QDialog, QVBoxLayout, QLabel, QTextEdit, QApplication
-from PyQt6.QtGui import QDesktopServices
-from PyQt6.QtCore import QUrl, Qt
-
-from classes.config_manager import ConfigManager
-from classes.utility import Utility
-
-class ShareImportDialog(QDialog):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setWindowTitle("Share/Import Settings")
- self.setModal(True)
- self.setFixedSize(400, 250)
- self.init_ui()
-
- def init_ui(self):
- layout = QVBoxLayout()
-
- # Label and text area for code input/output
- self.label = QLabel("Enter code to import or generate code to share:")
- self.code_input = QTextEdit()
- self.code_input.setFixedHeight(100)
-
- # Buttons
- self.import_button = QPushButton("Import Settings")
- self.export_button = QPushButton("Generate and Copy Code")
-
- # Button connections
- self.import_button.clicked.connect(self.import_settings)
- self.export_button.clicked.connect(self.export_settings)
+def populate_settings(main_window, frame):
+ """Populate the settings frame with configuration options."""
+ # Create a scrollable container for settings
+ settings = ctk.CTkScrollableFrame(
+ frame,
+ fg_color="transparent"
+ )
+ settings.pack(fill="both", expand=True, padx=40, pady=40)
+
+ # Frame for page title and subtitle
+ title_frame = ctk.CTkFrame(settings, fg_color="transparent")
+ title_frame.pack(fill="x", pady=(0, 40))
+
+ # Settings title with an icon
+ title_label = ctk.CTkLabel(
+ title_frame,
+ text="⚙️ Settings",
+ font=("Chivo", 36, "bold"),
+ text_color=("#1f2937", "#ffffff"),
+ anchor="w"
+ )
+ title_label.pack(side="left")
+
+ # Subtitle providing context
+ subtitle_label = ctk.CTkLabel(
+ title_frame,
+ text="Configure your CS2 bot preferences",
+ font=("Gambetta", 16),
+ text_color=("#64748b", "#94a3b8"),
+ anchor="w"
+ )
+ subtitle_label.pack(side="left", padx=(20, 0), pady=(10, 0))
+
+ # Create sections for trigger and timing settings
+ create_trigger_config_section(main_window, settings)
+ create_timing_settings_section(main_window, settings)
+
+ # Frame for action buttons
+ actions_frame = ctk.CTkFrame(
+ settings,
+ corner_radius=20,
+ fg_color=("#ffffff", "#1a1b23"),
+ border_width=2,
+ border_color=("#e2e8f0", "#2d3748")
+ )
+ actions_frame.pack(fill="x", pady=(40, 0))
+
+ # Content frame within actions section
+ actions_content = ctk.CTkFrame(actions_frame, fg_color="transparent")
+ actions_content.pack(fill="x", padx=40, pady=40)
+
+ # Header for configuration management
+ header_frame = ctk.CTkFrame(actions_content, fg_color="transparent")
+ header_frame.pack(fill="x", pady=(0, 30))
+
+ # Title for configuration management section
+ ctk.CTkLabel(
+ header_frame,
+ text="💾 Configuration Management",
+ font=("Chivo", 24, "bold"),
+ text_color=("#1f2937", "#ffffff"),
+ anchor="w"
+ ).pack(side="left")
+
+ # Description of configuration options
+ ctk.CTkLabel(
+ header_frame,
+ text="Save, reset, or manage your configuration",
+ font=("Gambetta", 14),
+ text_color=("#64748b", "#94a3b8"),
+ anchor="e"
+ ).pack(side="right")
+
+ # Frame for action buttons
+ buttons_frame = ctk.CTkFrame(actions_content, fg_color="transparent")
+ buttons_frame.pack(fill="x")
+
+ # Frame for primary action buttons (save and reset)
+ primary_frame = ctk.CTkFrame(buttons_frame, fg_color="transparent")
+ primary_frame.pack(side="left")
+
+ # Save settings button
+ save_btn = ctk.CTkButton(
+ primary_frame,
+ text="💾 Save Settings",
+ command=main_window.save_settings,
+ width=160,
+ height=50,
+ corner_radius=16,
+ fg_color=("#22c55e", "#16a34a"),
+ hover_color=("#16a34a", "#15803d"),
+ font=("Chivo", 16, "bold"),
+ border_width=2,
+ border_color=("#16a34a", "#15803d"),
+ anchor="center"
+ )
+ save_btn.pack(side="left", padx=(0, 15))
+
+ # Reset to defaults button
+ reset_btn = ctk.CTkButton(
+ primary_frame,
+ text="🔄 Reset Defaults",
+ command=main_window.reset_to_defaults,
+ width=160,
+ height=50,
+ corner_radius=16,
+ fg_color=("#6b7280", "#4b5563"),
+ hover_color=("#4b5563", "#374151"),
+ font=("Chivo", 16, "bold"),
+ border_width=2,
+ border_color=("#4b5563", "#374151"),
+ anchor="center"
+ )
+ reset_btn.pack(side="left")
+
+ # Frame for secondary action buttons (open config and share/import)
+ secondary_frame = ctk.CTkFrame(buttons_frame, fg_color="transparent")
+ secondary_frame.pack(side="right")
+
+ # Open config directory button
+ config_btn = ctk.CTkButton(
+ secondary_frame,
+ text="📁 Open Config",
+ command=main_window.open_config_directory,
+ width=140,
+ height=50,
+ corner_radius=16,
+ fg_color=("#3b82f6", "#2563eb"),
+ hover_color=("#2563eb", "#1d4ed8"),
+ font=("Chivo", 16, "bold"),
+ border_width=2,
+ border_color=("#2563eb", "#1d4ed8"),
+ anchor="center"
+ )
+ config_btn.pack(side="left", padx=(0, 15))
+
+ # Share/import settings button
+ import_btn = ctk.CTkButton(
+ secondary_frame,
+ text="📤 Share/Import",
+ command=main_window.show_share_import_dialog,
+ width=140,
+ height=50,
+ corner_radius=16,
+ fg_color=("#8b5cf6", "#7c3aed"),
+ hover_color=("#7c3aed", "#6d28d9"),
+ font=("Chivo", 16, "bold"),
+ border_width=2,
+ border_color=("#7c3aed", "#6d28d9"),
+ anchor="center"
+ )
+ import_btn.pack(side="left")
- # Add widgets to layout
- layout.addWidget(self.label)
- layout.addWidget(self.code_input)
- layout.addWidget(self.import_button)
- layout.addWidget(self.export_button)
+def create_trigger_config_section(main_window, parent):
+ """Create trigger configuration section with related settings."""
+ # Section frame with modern styling
+ section = ctk.CTkFrame(
+ parent,
+ corner_radius=20,
+ fg_color=("#ffffff", "#1a1b23"),
+ border_width=2,
+ border_color=("#e2e8f0", "#2d3748")
+ )
+ section.pack(fill="x", pady=(0, 30))
+
+ # Header frame for section title and description
+ header = ctk.CTkFrame(section, fg_color="transparent")
+ header.pack(fill="x", padx=40, pady=(40, 30))
+
+ # Section title with icon
+ ctk.CTkLabel(
+ header,
+ text="🎯 Trigger Configuration",
+ font=("Chivo", 24, "bold"),
+ text_color=("#1f2937", "#ffffff"),
+ anchor="w"
+ ).pack(side="left")
+
+ # Description of section purpose
+ ctk.CTkLabel(
+ header,
+ text="Control how the trigger responds",
+ font=("Gambetta", 14),
+ text_color=("#64748b", "#94a3b8"),
+ anchor="e"
+ ).pack(side="right")
+
+ # List of settings for trigger configuration
+ settings_list = [
+ ("Trigger Key", "entry", "trigger_key", "Key to activate trigger (e.g., 'x' or 'mouse4' for mouse button 4)"),
+ ("Toggle Mode", "checkbox", "toggle_mode", "Enable toggle mode instead of hold mode"),
+ ("Attack Teammates", "checkbox", "attack_teammates", "Allow triggering on teammates")
+ ]
+
+ # Create each setting item
+ for i, (label_text, widget_type, key, description) in enumerate(settings_list):
+ item_frame = create_setting_item(
+ section,
+ label_text,
+ description,
+ widget_type,
+ key,
+ main_window,
+ is_last=(i == len(settings_list) - 1)
+ )
- self.setLayout(layout)
+def create_timing_settings_section(main_window, parent):
+ """Create timing settings section for delay configurations."""
+ # Section frame with modern styling
+ section = ctk.CTkFrame(
+ parent,
+ corner_radius=20,
+ fg_color=("#ffffff", "#1a1b23"),
+ border_width=2,
+ border_color=("#e2e8f0", "#2d3748")
+ )
+ section.pack(fill="x", pady=(0, 30))
+
+ # Header frame for section title and description
+ header = ctk.CTkFrame(section, fg_color="transparent")
+ header.pack(fill="x", padx=40, pady=(40, 30))
+
+ # Section title with icon
+ ctk.CTkLabel(
+ header,
+ text="⏱️ Timing Settings",
+ font=("Chivo", 24, "bold"),
+ text_color=("#1f2937", "#ffffff"),
+ anchor="w"
+ ).pack(side="left")
+
+ # Description of section purpose
+ ctk.CTkLabel(
+ header,
+ text="Fine-tune shooting delays",
+ font=("Gambetta", 14),
+ text_color=("#64748b", "#94a3b8"),
+ anchor="e"
+ ).pack(side="right")
+
+ # List of settings for timing configuration
+ settings_list = [
+ ("Min Shot Delay", "entry", "min_delay", "Minimum delay between shots (seconds)"),
+ ("Max Shot Delay", "entry", "max_delay", "Maximum delay between shots (seconds)"),
+ ("Post Shot Delay", "entry", "post_delay", "Delay after shooting (seconds)")
+ ]
+
+ # Create each setting item
+ for i, (label_text, widget_type, key, description) in enumerate(settings_list):
+ item_frame = create_setting_item(
+ section,
+ label_text,
+ description,
+ widget_type,
+ key,
+ main_window,
+ is_last=(i == len(settings_list) - 1)
+ )
- def export_settings(self):
- # Gather current settings
- settings = {
- 'TriggerKey': self.parent().trigger_key_input.text(),
- 'ToggleMode': self.parent().toggle_mode_checkbox.isChecked(),
- 'ShotDelayMin': float(self.parent().min_delay_input.text() or 0.01),
- 'ShotDelayMax': float(self.parent().max_delay_input.text() or 0.1),
- 'PostShotDelay': float(self.parent().post_shot_delay_input.text() or 0.1),
- 'AttackOnTeammates': self.parent().attack_teammates_checkbox.isChecked()
- }
-
- # Serialize to JSON, compress with zlib, and encode to base64
- json_bytes = orjson.dumps(settings)
- compressed = zlib.compress(json_bytes)
- encoded = base64.b64encode(compressed).decode()
- code = f"TB-{encoded}"
-
- # Copy to clipboard using QApplication
- clipboard = QApplication.instance().clipboard()
- clipboard.setText(code)
+def create_setting_item(parent, label_text, description, widget_type, key, main_window, is_last=False):
+ """Create a standardized setting item with improved styling."""
+ # Frame for the setting item
+ item_frame = ctk.CTkFrame(parent, fg_color="transparent")
+ item_frame.pack(fill="x", padx=40, pady=(0, 30 if not is_last else 40))
+
+ # Container with hover effect
+ container = ctk.CTkFrame(
+ item_frame,
+ corner_radius=12,
+ fg_color=("#f8fafc", "#252830"),
+ border_width=1,
+ border_color=("#e2e8f0", "#374151")
+ )
+ container.pack(fill="x", pady=(0, 0))
+
+ # Content frame within the container
+ content_frame = ctk.CTkFrame(container, fg_color="transparent")
+ content_frame.pack(fill="x", padx=25, pady=25)
+
+ # Frame for label and description
+ label_frame = ctk.CTkFrame(content_frame, fg_color="transparent")
+ label_frame.pack(side="left", fill="x", expand=True)
+
+ # Setting name label
+ ctk.CTkLabel(
+ label_frame,
+ text=label_text,
+ font=("Chivo", 16, "bold"),
+ text_color=("#1f2937", "#ffffff"),
+ anchor="w"
+ ).pack(fill="x", pady=(0, 4))
+
+ # Description of the setting
+ ctk.CTkLabel(
+ label_frame,
+ text=description,
+ font=("Gambetta", 13),
+ text_color=("#64748b", "#94a3b8"),
+ anchor="w",
+ wraplength=400
+ ).pack(fill="x")
+
+ # Frame for the input widget
+ widget_frame = ctk.CTkFrame(content_frame, fg_color="transparent")
+ widget_frame.pack(side="right", padx=(30, 0))
+
+ # Create entry widget for text input
+ if widget_type == "entry":
+ widget = ctk.CTkEntry(
+ widget_frame,
+ width=220,
+ height=45,
+ corner_radius=12,
+ border_width=2,
+ border_color=("#d1d5db", "#374151"),
+ fg_color=("#ffffff", "#1f2937"),
+ text_color=("#1f2937", "#ffffff"),
+ font=("Chivo", 14),
+ justify="center"
+ )
+ widget.pack()
- # Show in text area and notify user
- self.code_input.setText(code)
- self.label.setText("Code copied to clipboard!")
-
- def import_settings(self):
- code = self.code_input.toPlainText().strip()
- if not code.startswith("TB-"):
- self.label.setText("Invalid code format. Must start with 'TB-'")
- return
+ # Assign widget to main_window based on key
+ if key == "trigger_key":
+ main_window.trigger_key_entry = widget
+ widget.insert(0, main_window.bot.config.get('Settings', {}).get('TriggerKey', ''))
+ elif key == "min_delay":
+ main_window.min_delay_entry = widget
+ widget.insert(0, str(main_window.bot.config.get('Settings', {}).get('ShotDelayMin', 0.01)))
+ elif key == "max_delay":
+ main_window.max_delay_entry = widget
+ widget.insert(0, str(main_window.bot.config.get('Settings', {}).get('ShotDelayMax', 0.03)))
+ elif key == "post_delay":
+ main_window.post_shot_delay_entry = widget
+ widget.insert(0, str(main_window.bot.config.get('Settings', {}).get('PostShotDelay', 0.1)))
+
+ # Create checkbox widget for boolean settings
+ elif widget_type == "checkbox":
+ if key == "toggle_mode":
+ main_window.toggle_mode_var = ctk.BooleanVar(value=main_window.bot.config.get('Settings', {}).get('ToggleMode', False))
+ widget = ctk.CTkCheckBox(
+ widget_frame,
+ text="",
+ variable=main_window.toggle_mode_var,
+ width=30,
+ height=30,
+ corner_radius=8,
+ border_width=2,
+ fg_color=("#D5006D", "#E91E63"),
+ hover_color=("#B8004A", "#C2185B"),
+ checkmark_color="#ffffff"
+ )
+ elif key == "attack_teammates":
+ main_window.attack_teammates_var = ctk.BooleanVar(value=main_window.bot.config.get('Settings', {}).get('AttackOnTeammates', False))
+ widget = ctk.CTkCheckBox(
+ widget_frame,
+ text="",
+ variable=main_window.attack_teammates_var,
+ width=30,
+ height=30,
+ corner_radius=8,
+ border_width=2,
+ fg_color=("#D5006D", "#E91E63"),
+ hover_color=("#B8004A", "#C2185B"),
+ checkmark_color="#ffffff"
+ )
- # Decode, decompress, and apply settings
- try:
- encoded = code[3:]
- compressed = base64.b64decode(encoded)
- json_bytes = zlib.decompress(compressed)
- settings = orjson.loads(json_bytes)
-
- # Apply settings to UI
- main_window = self.parent()
- main_window.trigger_key_input.setText(settings.get('TriggerKey', ''))
- main_window.toggle_mode_checkbox.setChecked(settings.get('ToggleMode', False))
- main_window.min_delay_input.setText(str(settings.get('ShotDelayMin', 0.01)))
- main_window.max_delay_input.setText(str(settings.get('ShotDelayMax', 0.1)))
- main_window.post_shot_delay_input.setText(str(settings.get('PostShotDelay', 0.1)))
- main_window.attack_teammates_checkbox.setChecked(settings.get('AttackOnTeammates', False))
-
- # Save the imported settings using the module function
- save_general_settings(main_window)
-
- self.label.setText("Settings imported and saved successfully!")
- except Exception as e:
- self.label.setText(f"Error importing settings: {str(e)}")
-
-def init_general_settings_tab(main_window):
- """
- Sets up the General Settings tab for configuring the bot.
- Creates input fields and buttons, then attaches them to the main window.
- """
- general_settings_tab = QWidget()
- form_layout = QFormLayout()
-
- settings = main_window.bot.config.get('Settings', {})
-
- main_window.trigger_key_input = QLineEdit(settings.get('TriggerKey', ''))
- main_window.trigger_key_input.setToolTip("Set the key to activate the trigger bot (e.g., 'x' or 'x1' for mouse button 4, 'x2' for mouse button 5).")
- main_window.toggle_mode_checkbox = QCheckBox("Toggle Mode")
- main_window.toggle_mode_checkbox.setChecked(settings.get('ToggleMode', False))
- main_window.toggle_mode_checkbox.setToolTip("If checked, the trigger will toggle on/off with the trigger key.")
- main_window.min_delay_input = QLineEdit(str(settings.get('ShotDelayMin', 0.01)))
- main_window.min_delay_input.setToolTip("Minimum delay between shots in seconds (e.g., 0.01).")
- main_window.max_delay_input = QLineEdit(str(settings.get('ShotDelayMax', 0.1)))
- main_window.max_delay_input.setToolTip("Maximum delay between shots in seconds (must be >= Min Delay).")
- main_window.post_shot_delay_input = QLineEdit(str(settings.get('PostShotDelay', 0.1)))
- main_window.post_shot_delay_input.setToolTip("Delay after each shot in seconds (e.g., 0.1).")
- main_window.attack_teammates_checkbox = QCheckBox("Attack Teammates")
- main_window.attack_teammates_checkbox.setChecked(settings.get('AttackOnTeammates', False))
- main_window.attack_teammates_checkbox.setToolTip("If checked, the bot will attack teammates as well.")
-
- save_button = QPushButton("Save Config")
- save_button.setToolTip("Save the configuration settings to the configuration file.")
- save_button.clicked.connect(lambda: save_general_settings(main_window))
- open_config_button = QPushButton("Open Config Directory")
- open_config_button.setToolTip("Open the directory where the configuration file is stored.")
- open_config_button.clicked.connect(lambda: QDesktopServices.openUrl(QUrl.fromLocalFile(ConfigManager.CONFIG_DIRECTORY)))
- share_import_button = QPushButton("Share/Import")
- share_import_button.setToolTip("Open a dialog to share or import settings.")
- share_import_button.clicked.connect(lambda: ShareImportDialog(main_window).exec())
- reset_button = QPushButton("Reset to Defaults")
- reset_button.setToolTip("Reset all settings to their default values.")
- reset_button.clicked.connect(lambda: reset_to_defaults(main_window))
-
- # First checkbox layout for Toggle Mode and Attack Teammates
- checkbox_layout_1 = QHBoxLayout()
- checkbox_layout_1.addWidget(main_window.toggle_mode_checkbox)
- checkbox_layout_1.addWidget(main_window.attack_teammates_checkbox)
-
- button_layout = QHBoxLayout()
- button_layout.addWidget(save_button)
- button_layout.addWidget(open_config_button)
- button_layout.addWidget(share_import_button)
- button_layout.addWidget(reset_button)
-
- form_layout.addRow("Trigger Key:", main_window.trigger_key_input)
- form_layout.addRow(checkbox_layout_1)
- form_layout.addRow("Min Shot Delay:", main_window.min_delay_input)
- form_layout.addRow("Max Shot Delay:", main_window.max_delay_input)
- form_layout.addRow("Post Shot Delay:", main_window.post_shot_delay_input)
- form_layout.addItem(QSpacerItem(15, 15, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding))
- form_layout.addRow(button_layout)
-
- general_settings_tab.setLayout(form_layout)
- main_window.tabs.addTab(general_settings_tab, "General Settings")
-
-def save_general_settings(main_window):
- """
- Saves the user's changes to the bot's configuration:
- - Validates user inputs.
- - Updates the bot's configuration with new values.
- - Persists the updated configuration using the ConfigManager.
- """
- try:
- validate_inputs(main_window)
- settings = main_window.bot.config['Settings']
- settings['TriggerKey'] = main_window.trigger_key_input.text().strip()
- settings['ToggleMode'] = main_window.toggle_mode_checkbox.isChecked()
- settings['AttackOnTeammates'] = main_window.attack_teammates_checkbox.isChecked()
- settings['ShotDelayMin'] = float(main_window.min_delay_input.text())
- settings['ShotDelayMax'] = float(main_window.max_delay_input.text())
- settings['PostShotDelay'] = float(main_window.post_shot_delay_input.text())
- ConfigManager.save_config(main_window.bot.config)
- main_window.bot.update_config(main_window.bot.config)
- from PyQt6.QtWidgets import QMessageBox
- QMessageBox.information(main_window, "Settings Saved", "Configuration has been successfully saved.")
- except ValueError as e:
- from PyQt6.QtWidgets import QMessageBox
- QMessageBox.critical(main_window, "Invalid Input", str(e))
-
-def validate_inputs(main_window):
- """
- Validates user input fields in the General Settings tab.
- Ensures all required fields have valid values.
- """
- trigger_key = main_window.trigger_key_input.text().strip()
- if not trigger_key:
- raise ValueError("Trigger key cannot be empty.")
-
- try:
- min_delay = float(main_window.min_delay_input.text().strip())
- max_delay = float(main_window.max_delay_input.text().strip())
- post_delay = float(main_window.post_shot_delay_input.text().strip())
- except ValueError:
- raise ValueError("Delay values must be valid numbers.")
-
- if min_delay < 0 or max_delay < 0 or post_delay < 0:
- raise ValueError("Delay values must be non-negative.")
- if min_delay > max_delay:
- raise ValueError("Minimum delay cannot be greater than maximum delay.")
-
-def reset_to_defaults(main_window):
- """
- Resets all settings in the General Settings tab to their default values
- as defined in ConfigManager.DEFAULT_CONFIG, then сохраняет их.
- """
- defaults = ConfigManager.DEFAULT_CONFIG['Settings']
-
- main_window.trigger_key_input.setText(defaults.get('TriggerKey', ''))
- main_window.toggle_mode_checkbox.setChecked(defaults.get('ToggleMode', False))
- main_window.min_delay_input.setText(str(defaults.get('ShotDelayMin', 0.01)))
- main_window.max_delay_input.setText(str(defaults.get('ShotDelayMax', 0.1)))
- main_window.post_shot_delay_input.setText(str(defaults.get('PostShotDelay', 0.1)))
- main_window.attack_teammates_checkbox.setChecked(defaults.get('AttackOnTeammates', False))
-
- save_general_settings(main_window)
\ No newline at end of file
+ widget.pack()
+
+ return item_frame
\ No newline at end of file
diff --git a/gui/home_tab.py b/gui/home_tab.py
index 486cb68..50c3518 100644
--- a/gui/home_tab.py
+++ b/gui/home_tab.py
@@ -1,73 +1,326 @@
-from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
-from PyQt6.QtCore import Qt
-
+import customtkinter as ctk
+import threading
+import orjson
+from classes.logger import Logger
from classes.utility import Utility
+from classes.config_manager import ConfigManager
-def init_home_tab(main_window):
- """
- Sets up the Home tab with bot instructions, status, and control buttons.
- Attaches key widgets to the main window (e.g. status_label, start_button).
- """
- home_tab = QWidget()
- layout = QVBoxLayout()
- layout.setSpacing(15)
-
- # Bot status label
- main_window.status_label = QLabel("Bot Status: Inactive")
- main_window.status_label.setStyleSheet("font-size: 16px; color: #FF5252; font-weight: bold;")
+# Cache the logger instance
+logger = Logger.get_logger()
- # Last offsets update label
- main_window.last_update_label = QLabel("Last offsets update: Fetching...")
- main_window.last_update_label.setStyleSheet("font-size: 14px; font-style: italic; color: #B0B0B0;")
- Utility.fetch_last_offset_update(main_window.last_update_label)
-
- # Quick start guide
- layout.addWidget(create_section_label("Quick Start Guide", 18))
- quick_start_text = QLabel(
- "1. Open CS2 game and ensure it’s running.
"
- "2. Configure trigger key and delays in General Settings.
"
- "3. Press Start Bot to activate.
"
- "4. Monitor bot status and logs in the 'Logs' tab."
+def populate_dashboard(main_window, frame):
+ """Populate the dashboard frame with status cards, controls, and a quick start guide."""
+ # Scrollable container for dashboard content
+ dashboard = ctk.CTkScrollableFrame(
+ frame,
+ fg_color="transparent"
)
- quick_start_text.setStyleSheet("font-size: 14px;")
- quick_start_text.setTextFormat(Qt.TextFormat.RichText)
- quick_start_text.setWordWrap(True)
- layout.addWidget(quick_start_text)
-
- # Additional Information
- layout.addWidget(create_section_label("Additional Information", 18))
- additional_info_text = QLabel(
- "For more details, visit our "
- "GitHub repository "
- "or join our "
- "Telegram channel."
+ dashboard.pack(fill="both", expand=True, padx=40, pady=40)
+
+ # Frame for page title and subtitle
+ title_frame = ctk.CTkFrame(dashboard, fg_color="transparent")
+ title_frame.pack(fill="x", pady=(0, 30))
+
+ # Dashboard title with icon
+ title_label = ctk.CTkLabel(
+ title_frame,
+ text="🎯 Dashboard",
+ font=("Chivo", 36, "bold"),
+ text_color=("#1f2937", "#ffffff")
)
- additional_info_text.setStyleSheet("font-size: 14px;")
- additional_info_text.setTextFormat(Qt.TextFormat.RichText)
- additional_info_text.setOpenExternalLinks(True)
- additional_info_text.setWordWrap(True)
- layout.addWidget(additional_info_text)
-
- # Bot control buttons
- main_window.start_button = QPushButton("Start Bot")
- main_window.stop_button = QPushButton("Stop Bot")
- main_window.start_button.clicked.connect(main_window.start_bot)
- main_window.stop_button.clicked.connect(main_window.stop_bot)
-
- buttons_layout = QHBoxLayout()
- buttons_layout.addWidget(main_window.start_button)
- buttons_layout.addWidget(main_window.stop_button)
- buttons_layout.setSpacing(20)
+ title_label.pack(side="left")
+
+ # Subtitle providing context
+ subtitle_label = ctk.CTkLabel(
+ title_frame,
+ text="Monitor and control your CS2 bot",
+ font=("Gambetta", 16),
+ text_color=("#64748b", "#94a3b8")
+ )
+ subtitle_label.pack(side="left", padx=(20, 0), pady=(10, 0))
+
+ # Frame for status cards
+ stats_frame = ctk.CTkFrame(dashboard, fg_color="transparent")
+ stats_frame.pack(fill="x", pady=(0, 40))
+
+ # Bot status card with stored label reference
+ status_card, main_window.bot_status_label = create_stat_card(
+ main_window,
+ stats_frame,
+ "🤖 Bot Status",
+ "Inactive",
+ "#ef4444",
+ "Current operational state"
+ )
+ status_card.pack(side="left", fill="x", expand=True, padx=(0, 20))
+
+ # Last update card with stored label reference
+ update_card, main_window.update_value_label = create_stat_card(
+ main_window,
+ stats_frame,
+ "🔄 Offsets Update",
+ "Checking...",
+ "#6b7280",
+ "Last offsets synchronization"
+ )
+ update_card.pack(side="left", fill="x", expand=True, padx=(10, 10))
+
+ # Version card
+ version_card, version_value_label = create_stat_card(
+ main_window,
+ stats_frame,
+ "📦 Version",
+ f"{ConfigManager.VERSION}",
+ "#D5006D",
+ "Current application version"
+ )
+ version_card.pack(side="left", fill="x", expand=True, padx=(20, 0))
+
+ # Control panel section
+ control_panel = ctk.CTkFrame(
+ dashboard,
+ corner_radius=20,
+ fg_color=("#ffffff", "#1a1b23"),
+ border_width=2,
+ border_color=("#e2e8f0", "#2d3748")
+ )
+ control_panel.pack(fill="x", pady=(0, 40))
+
+ # Header for control panel
+ control_header = ctk.CTkFrame(control_panel, fg_color="transparent")
+ control_header.pack(fill="x", padx=40, pady=(40, 30))
+
+ # Control center title
+ ctk.CTkLabel(
+ control_header,
+ text="🎮 Bot Control Center",
+ font=("Chivo", 24, "bold"),
+ text_color=("#1f2937", "#ffffff")
+ ).pack(side="left")
+
+ # Frame for control buttons
+ control_buttons = ctk.CTkFrame(control_panel, fg_color="transparent")
+ control_buttons.pack(fill="x", padx=40, pady=(0, 40))
+
+ # Start button with play icon
+ start_button = ctk.CTkButton(
+ control_buttons,
+ text="▶ Start Bot",
+ command=main_window.start_bot,
+ width=180,
+ height=60,
+ corner_radius=16,
+ fg_color=("#22c55e", "#16a34a"),
+ hover_color=("#16a34a", "#15803d"),
+ font=("Chivo", 18, "bold"),
+ border_width=2,
+ border_color=("#16a34a", "#15803d")
+ )
+ start_button.pack(side="left", padx=(0, 20))
+
+ # Stop button with stop icon
+ stop_button = ctk.CTkButton(
+ control_buttons,
+ text="⏹ Stop Bot",
+ command=main_window.stop_bot,
+ width=180,
+ height=60,
+ corner_radius=16,
+ fg_color=("#ef4444", "#dc2626"),
+ hover_color=("#dc2626", "#b91c1c"),
+ font=("Chivo", 18, "bold"),
+ border_width=2,
+ border_color=("#dc2626", "#b91c1c")
+ )
+ stop_button.pack(side="left")
+
+ # Quick start guide section
+ guide_card = ctk.CTkFrame(
+ dashboard,
+ corner_radius=20,
+ fg_color=("#ffffff", "#1a1b23"),
+ border_width=2,
+ border_color=("#e2e8f0", "#2d3748")
+ )
+ guide_card.pack(fill="x")
+
+ # Header for quick start guide
+ guide_header = ctk.CTkFrame(guide_card, fg_color="transparent")
+ guide_header.pack(fill="x", padx=40, pady=(40, 30))
+
+ # Guide title with icon
+ ctk.CTkLabel(
+ guide_header,
+ text="🚀 Quick Start Guide",
+ font=("Chivo", 24, "bold"),
+ text_color=("#1f2937", "#ffffff")
+ ).pack(side="left")
+
+ # Guide subtitle
+ ctk.CTkLabel(
+ guide_header,
+ text="Follow these steps to get started",
+ font=("Gambetta", 14),
+ text_color=("#64748b", "#94a3b8")
+ ).pack(side="right")
+
+ # List of guide steps
+ steps = [
+ ("1", "Launch CS2", "Open Counter-Strike 2 and ensure it's running"),
+ ("2", "Configure Settings", "Set your trigger key and adjust delays in Settings"),
+ ("3", "Start Bot", "Click the Start Bot button to activate"),
+ ("4", "Monitor Logs", "Check the Logs tab for activity and status updates")
+ ]
+
+ # Create each step
+ for i, (step_num, step_title, step_desc) in enumerate(steps):
+ # Frame for the step
+ step_frame = ctk.CTkFrame(guide_card, fg_color="transparent")
+ step_frame.pack(fill="x", padx=40, pady=(0, 25 if i < len(steps)-1 else 40))
+
+ # Step number badge
+ step_badge = ctk.CTkFrame(
+ step_frame,
+ width=50,
+ height=50,
+ corner_radius=25,
+ fg_color=("#D5006D", "#E91E63")
+ )
+ step_badge.pack(side="left", padx=(0, 25))
+ step_badge.pack_propagate(False)
+
+ # Step number inside badge
+ ctk.CTkLabel(
+ step_badge,
+ text=step_num,
+ font=("Chivo", 20, "bold"),
+ text_color="#ffffff"
+ ).place(relx=0.5, rely=0.5, anchor="center")
+
+ # Frame for step content
+ step_content = ctk.CTkFrame(step_frame, fg_color="transparent")
+ step_content.pack(side="left", fill="x", expand=True)
+
+ # Step title
+ ctk.CTkLabel(
+ step_content,
+ text=step_title,
+ font=("Chivo", 18, "bold"),
+ text_color=("#1f2937", "#ffffff"),
+ anchor="w"
+ ).pack(fill="x")
+
+ # Step description
+ ctk.CTkLabel(
+ step_content,
+ text=step_desc,
+ font=("Gambetta", 14),
+ text_color=("#64748b", "#94a3b8"),
+ anchor="w"
+ ).pack(fill="x", pady=(4, 0))
+
+ # Connector line between steps (except last)
+ if i < len(steps) - 1:
+ connector = ctk.CTkFrame(
+ guide_card,
+ width=2,
+ height=20,
+ fg_color=("#e2e8f0", "#374151")
+ )
+ connector.pack(padx=(65, 0), anchor="w")
+
+ # Fetch last update timestamp
+ fetch_last_update(main_window)
- layout.addWidget(main_window.status_label)
- layout.addWidget(main_window.last_update_label)
- layout.addLayout(buttons_layout)
+def create_stat_card(main_window, parent, title, value, color, subtitle):
+ """Create a modern stat card and return the card and value label."""
+ # Card frame with modern styling
+ card = ctk.CTkFrame(
+ parent,
+ corner_radius=20,
+ fg_color=("#ffffff", "#1a1b23"),
+ border_width=2,
+ border_color=("#e2e8f0", "#2d3748")
+ )
+
+ # Content frame within card
+ content = ctk.CTkFrame(card, fg_color="transparent")
+ content.pack(fill="both", expand=True, padx=30, pady=30)
+
+ # Card header
+ ctk.CTkLabel(
+ content,
+ text=title,
+ font=("Chivo", 16, "bold"),
+ text_color=("#64748b", "#94a3b8"),
+ anchor="w"
+ ).pack(fill="x", pady=(0, 12))
+
+ # Value label with dynamic color
+ value_label = ctk.CTkLabel(
+ content,
+ text=value,
+ font=("Chivo", 28, "bold"),
+ text_color=color,
+ anchor="w"
+ )
+ value_label.pack(fill="x", pady=(0, 8))
+
+ # Subtitle providing context
+ ctk.CTkLabel(
+ content,
+ text=subtitle,
+ font=("Gambetta", 13),
+ text_color=("#94a3b8", "#64748b"),
+ anchor="w"
+ ).pack(fill="x")
+
+ return card, value_label
- home_tab.setLayout(layout)
- main_window.tabs.addTab(home_tab, "Home")
+def fetch_last_update(main_window):
+ """Fetch and display the last offset update time."""
+ def update_callback():
+ try:
+ import requests
+ from dateutil.parser import parse as parse_date
+
+ # Fetch latest commit data from GitHub
+ response = requests.get("https://api.github.com/repos/a2x/cs2-dumper/commits/main")
+ response.raise_for_status()
+ commit_data = orjson.loads(response.content)
+ commit_timestamp = commit_data["commit"]["committer"]["date"]
+
+ # Parse and format the timestamp
+ last_update_dt = parse_date(commit_timestamp)
+ formatted_timestamp = last_update_dt.strftime("%m/%d/%Y %H:%M")
+
+ # Update UI with formatted timestamp
+ main_window.root.after(0, lambda: main_window.update_value_label.configure(
+ text=formatted_timestamp, text_color="#22c55e"
+ ))
+ except Exception as e:
+ # Display error if fetch fails
+ main_window.root.after(0, lambda: main_window.update_value_label.configure(
+ text="Error", text_color="#ef4444"
+ ))
+ logger.error("Failed to fetch last update: %s", e)
+
+ # Run fetch in a separate thread
+ threading.Thread(target=update_callback, daemon=True).start()
-def create_section_label(text, font_size):
- """Helper to create a styled section label."""
- label = QLabel(text)
- label.setStyleSheet(f"font-size: {font_size}px; font-weight: bold; color: #D5006D; margin-top: 10px;")
- return label
\ No newline at end of file
+def update_bot_status(self, status, color):
+ """Update the bot status indicators across the dashboard."""
+ # Update header status label
+ self.status_label.configure(text=status, text_color=color)
+ # Update dashboard status label
+ self.bot_status_label.configure(text=status, text_color=color)
+
+ # Update status dot color in header
+ for widget in self.status_frame.winfo_children():
+ if isinstance(widget, ctk.CTkFrame) and widget.cget("width") == 12:
+ widget.configure(fg_color=color)
+ break
+
+ # Ensure dashboard status updates if widget exists
+ if hasattr(self, 'bot_status_label') and self.bot_status_label.winfo_exists():
+ self.bot_status_label.configure(text=status, text_color=color)
\ No newline at end of file
diff --git a/gui/logs_tab.py b/gui/logs_tab.py
index 602e83b..81e43de 100644
--- a/gui/logs_tab.py
+++ b/gui/logs_tab.py
@@ -1,57 +1,213 @@
+import customtkinter as ctk
import os
+from classes.logger import Logger
-from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTextEdit
-from PyQt6.QtCore import QTimer
+def populate_logs(main_window, frame):
+ """Populate the logs frame with a text widget to display logs."""
+ # Clear existing widgets to prevent duplication
+ for widget in frame.winfo_children():
+ widget.destroy()
-from classes.logger import Logger
+ # Container for all logs UI elements with padding
+ logs_container = ctk.CTkFrame(
+ frame,
+ fg_color="transparent"
+ )
+ logs_container.pack(fill="both", expand=True, padx=24, pady=24)
-def init_logs_tab(main_window):
- """
- Sets up the Logs tab to display application logs.
- Attaches a read-only text area to the main window.
- """
- logs_tab = QWidget()
- layout = QVBoxLayout()
- main_window.log_output = QTextEdit()
- main_window.log_output.setReadOnly(True)
-
- # Initialize log tracking
- main_window.last_log_position = 0
- main_window.log_timer = QTimer(main_window)
- main_window.log_timer.timeout.connect(lambda: update_log_output(main_window))
- main_window.log_timer.start(1000) # Update logs every second
-
- layout.addWidget(main_window.log_output)
- logs_tab.setLayout(layout)
- main_window.tabs.addTab(logs_tab, "Logs")
-
-def update_log_output(main_window):
- """
- Periodically updates the Logs tab with new log entries from the log file.
- Appends new log entries since the last read position to the log display.
- """
- try:
- # Check the current size of the log file.
- file_size = os.path.getsize(Logger.LOG_FILE)
+ # Header section with fixed height
+ header_frame = ctk.CTkFrame(
+ logs_container,
+ fg_color="transparent",
+ height=90
+ )
+ header_frame.pack(fill="x", pady=(0, 28))
+ header_frame.pack_propagate(False)
+
+ # Container for title and subtitle
+ title_container = ctk.CTkFrame(header_frame, fg_color="transparent")
+ title_container.pack(side="left", fill="y")
+
+ # Title label with bold styling
+ title_label = ctk.CTkLabel(
+ title_container,
+ text="📋 Application Logs",
+ font=("Chivo", 32, "bold"),
+ text_color=("#1f2937", "#f9fafb")
+ )
+ title_label.pack(anchor="w", pady=(8, 0))
+
+ # Subtitle providing context
+ subtitle_label = ctk.CTkLabel(
+ title_container,
+ text="Real-time application logs and system events",
+ font=("Gambetta", 15),
+ text_color=("#6b7280", "#9ca3af")
+ )
+ subtitle_label.pack(anchor="w", pady=(4, 0))
+
+ # Main card for logs display
+ logs_card = ctk.CTkFrame(
+ logs_container,
+ corner_radius=16,
+ fg_color=("#ffffff", "#18181b"),
+ border_width=1,
+ border_color=("#e2e8f0", "#27272a")
+ )
+ logs_card.pack(fill="both", expand=True)
+
+ # Header bar within the logs card
+ logs_header = ctk.CTkFrame(
+ logs_card,
+ height=60,
+ fg_color=("#f8fafc", "#27272a"),
+ border_width=0
+ )
+ logs_header.pack(fill="x", padx=2, pady=(2, 0))
+ logs_header.pack_propagate(False)
+
+ # Content frame for header elements
+ header_content = ctk.CTkFrame(logs_header, fg_color="transparent")
+ header_content.pack(fill="both", expand=True, padx=24, pady=16)
+
+ # Logs section title
+ logs_title = ctk.CTkLabel(
+ header_content,
+ text="System Logs",
+ font=("Chivo", 18, "bold"),
+ text_color=("#1f2937", "#f1f5f9")
+ )
+ logs_title.pack(side="left")
- # If the file has been truncated or rotated, reset the position.
- if file_size < main_window.last_log_position:
- main_window.last_log_position = 0
+ # Status indicator frame
+ status_frame = ctk.CTkFrame(header_content, fg_color="transparent")
+ status_frame.pack(side="right")
- # If there's no new content, exit early.
- if main_window.last_log_position == file_size:
+ # Status dot indicating live updates
+ status_dot = ctk.CTkLabel(
+ status_frame,
+ text="●",
+ font=("Chivo", 14),
+ text_color=("#059669", "#10b981")
+ )
+ status_dot.pack(side="left", padx=(0, 8))
+
+ # Status text "Live"
+ status_text = ctk.CTkLabel(
+ status_frame,
+ text="Live",
+ font=("Chivo", 14, "bold"),
+ text_color=("#059669", "#10b981")
+ )
+ status_text.pack(side="left")
+
+ # Content area for log text
+ logs_content = ctk.CTkFrame(
+ logs_card,
+ corner_radius=0,
+ fg_color="transparent"
+ )
+ logs_content.pack(fill="both", expand=True, padx=2, pady=(0, 2))
+
+ # Text widget to display logs
+ main_window.log_text = ctk.CTkTextbox(
+ logs_content,
+ corner_radius=0,
+ border_width=0,
+ font=("Chivo", 13),
+ fg_color=("#fcfcfd", "#0f0f11"),
+ text_color=("#1f2937", "#e2e8f0"),
+ state="disabled",
+ wrap="word"
+ )
+ main_window.log_text.pack(fill="both", expand=True, padx=20, pady=20)
+
+ # Load existing logs and set initial position
+ _load_logs_safely(main_window)
+ if os.path.exists(Logger.LOG_FILE):
+ main_window.last_log_position = os.path.getsize(Logger.LOG_FILE)
+ else:
+ main_window.last_log_position = 0
+
+def _load_logs_safely(main_window):
+ """Safely load logs with duplicate prevention and proper error handling."""
+ logger = Logger.get_logger()
+ try:
+ # Display welcome message if log file doesn’t exist
+ if not os.path.exists(Logger.LOG_FILE):
+ welcome_msg = (
+ "=== Application Logs ===\n"
+ "Welcome to the logs viewer!\n"
+ "Logs will appear here as the application runs.\n\n"
+ "[INFO] Logger initialized successfully\n"
+ "[INFO] Logs tab loaded\n"
+ )
+ _replace_content(main_window, welcome_msg)
return
- with open(Logger.LOG_FILE, 'r') as log_file:
- log_file.seek(main_window.last_log_position)
- new_logs = log_file.read()
- # Update the last read position.
- main_window.last_log_position = log_file.tell()
+ # Read all log lines from the file
+ with open(Logger.LOG_FILE, 'r', encoding='utf-8') as log_file:
+ raw_lines = log_file.read().splitlines()
+
+ # Get currently displayed lines to avoid duplicates
+ displayed = main_window.log_text.get("1.0", "end-1c").splitlines()
+ # Filter out duplicates
+ new_lines = [line for line in raw_lines if line not in displayed]
- if new_logs:
- main_window.log_output.insertPlainText(new_logs)
- main_window.log_output.ensureCursorVisible()
+ if new_lines:
+ main_window.log_text.configure(state="normal")
+ if not displayed:
+ # Initial load: replace all content
+ main_window.log_text.delete("1.0", "end")
+ main_window.log_text.insert("1.0", "\n".join(new_lines) + "\n")
+ else:
+ # Append new lines only
+ main_window.log_text.insert("end", "\n".join(new_lines) + "\n")
+ main_window.log_text.see("end")
+ main_window.log_text.configure(state="disabled")
+ elif not displayed:
+ # Display message if log file is empty
+ empty_msg = (
+ "=== Application Logs ===\n"
+ "Log file exists but is empty.\n"
+ "New logs will appear here as they are generated.\n\n"
+ "[INFO] Empty log file detected\n"
+ )
+ _replace_content(main_window, empty_msg)
+ except FileNotFoundError:
+ logger.warning(f"Log file {Logger.LOG_FILE} not found")
+ _show_error_message(main_window, "Log file not found")
+ except PermissionError:
+ logger.error(f"Permission denied reading log file {Logger.LOG_FILE}")
+ _show_error_message(main_window, "Permission denied accessing log file")
+ except UnicodeDecodeError:
+ logger.error(f"Encoding error reading log file {Logger.LOG_FILE}")
+ _show_error_message(main_window, "Log file encoding error")
except Exception as e:
- main_window.log_output.append(f"Failed to read log file: {e}")
- main_window.log_output.ensureCursorVisible()
\ No newline at end of file
+ logger.error(f"Unexpected error loading logs: {e}")
+ _show_error_message(main_window, f"Error loading logs: {str(e)}")
+
+def _replace_content(main_window, text):
+ """Helper to replace the entire content of the log widget."""
+ # Enable widget, clear content, insert new text, and disable again
+ main_window.log_text.configure(state="normal")
+ main_window.log_text.delete("1.0", "end")
+ main_window.log_text.insert("1.0", text)
+ main_window.log_text.configure(state="disabled")
+
+def _show_error_message(main_window, error_msg):
+ """Display error message in the logs text area."""
+ # Format error message with guidance
+ error_display = (
+ "=== Application Logs ===\n"
+ "❌ Error Loading Logs\n\n"
+ f"{error_msg}\n\n"
+ "Please check the application logs directory and permissions.\n"
+ "Try refreshing the logs tab or restarting the application.\n\n"
+ f"[ERROR] {error_msg}\n"
+ )
+ main_window.log_text.configure(state="normal")
+ main_window.log_text.delete("1.0", "end")
+ main_window.log_text.insert("1.0", error_display)
+ main_window.log_text.configure(state="disabled")
\ No newline at end of file
diff --git a/gui/main_window.py b/gui/main_window.py
index 6131eee..d4b62d5 100644
--- a/gui/main_window.py
+++ b/gui/main_window.py
@@ -1,10 +1,17 @@
-import os, threading
-
-from PyQt6.QtCore import Qt, QTimer, QUrl, QSize
-from PyQt6.QtWidgets import (QMainWindow, QPushButton, QLabel, QLineEdit, QTextEdit,
- QCheckBox, QVBoxLayout, QHBoxLayout, QWidget, QMessageBox,
- QFormLayout, QTabWidget, QSpacerItem, QSizePolicy)
-from PyQt6.QtGui import QIcon, QDesktopServices
+import os
+import threading
+import time
+import webbrowser
+import subprocess
+import platform
+import customtkinter as ctk
+from tkinter import messagebox
+from PIL import Image, ImageTk
+import orjson
+import base64
+import zlib
+import requests
+import sys
from watchdog.observers import Observer
@@ -14,71 +21,498 @@
from classes.file_watcher import ConfigFileChangeHandler
from classes.logger import Logger
-from gui.home_tab import init_home_tab
-from gui.general_settings_tab import init_general_settings_tab
-from gui.logs_tab import init_logs_tab
-from gui.faq_tab import init_faq_tab
+from gui.home_tab import populate_dashboard
+from gui.general_settings_tab import populate_settings
+from gui.logs_tab import populate_logs
+from gui.faq_tab import populate_faq
+from gui.supporters_tab import populate_supporters
-# Cache the logger instance
+# Cache the logger instance for consistent logging throughout the application
logger = Logger.get_logger()
-class MainWindow(QMainWindow):
+class MainWindow:
def __init__(self):
- """
- Initialize the main application window and setup UI components.
- """
- super().__init__()
-
+ """Initialize the main application window and setup UI components."""
+ # Define repository URL for reference
self.repo_url = "github.com/Jesewe/cs2-triggerbot"
- self.setWindowTitle(f"CS2 TriggerBot | {self.repo_url}")
- self.setFixedSize(700, 500)
-
- # Load and apply custom styles from external stylesheet.
- self.apply_stylesheet(Utility.resource_path('src/styles.css'))
-
- # Set application icon if available.
- self.set_app_icon(Utility.resource_path('src/img/icon.png'))
-
- # Create main layout with tabs.
- self.main_layout = QVBoxLayout()
- self.tabs = QTabWidget()
-
- # Fetch offsets and initialize the TriggerBot.
+ # Initialize bot thread, observer, and log timer as None until set up
+ self.bot_thread = None
+ self.observer = None
+ self.log_timer = None
+ # Track the last position in the log file for incremental updates
+ self.last_log_position = 0
+
+ # Configure CustomTkinter with a modern dark theme
+ ctk.set_appearance_mode("dark")
+ ctk.set_default_color_theme("blue")
+
+ # Fetch offsets and client data, initialize the TriggerBot instance
offsets, client_data = self.fetch_offsets_or_warn()
self.bot = CS2TriggerBot(offsets, client_data)
+
+ # Create the main window with a title and initial size
+ self.root = ctk.CTk()
+ self.root.title(f"CS2 TriggerBot {ConfigManager.VERSION}")
+ self.root.geometry("1300x700")
+ self.root.resizable(True, True)
+ self.root.minsize(1300, 700)
+
+ # Set the window icon using a resource path utility
+ self.root.iconbitmap(Utility.resource_path('src/img/icon.ico'))
+
+ # Load custom fonts on Windows systems
+ if platform.system() == "Windows":
+ import ctypes
+ gdi32 = ctypes.WinDLL('gdi32')
+ font_files = [
+ 'src/fonts/Chivo-Regular.ttf',
+ 'src/fonts/Chivo-Bold.ttf',
+ 'src/fonts/Gambetta-Regular.ttf',
+ 'src/fonts/Gambetta-Bold.ttf'
+ ]
+ for font_file in font_files:
+ font_path = Utility.resource_path(font_file)
+ if os.path.exists(font_path):
+ gdi32.AddFontResourceW(font_path)
+ else:
+ logger.warning(f"Font file not found: {font_path}")
+
+ # Configure grid layout to make the UI responsive
+ self.root.grid_columnconfigure(0, weight=1)
+ self.root.grid_rowconfigure(1, weight=1)
+
+ # Initialize UI components like header and content
+ self.setup_ui()
+
+ # Set up configuration file watcher and log update timer
+ self.init_config_watcher()
+ self.start_log_timer()
+
+ # Bind window close event to cleanup resources
+ self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
+
+ def setup_ui(self):
+ """Setup the modern user interface components."""
+ # Create a modern header with branding and controls
+ self.create_modern_header()
+
+ # Create the main content area including sidebar navigation
+ self.create_main_content()
+
+ def create_modern_header(self):
+ """Create a sleek modern header with gradient-like appearance."""
+ # Main header container with fixed height and dark background
+ header_container = ctk.CTkFrame(
+ self.root,
+ height=80,
+ corner_radius=0,
+ fg_color=("#1a1a1a", "#0d1117")
+ )
+ header_container.grid(row=0, column=0, sticky="ew", padx=0, pady=0)
+ header_container.grid_propagate(False)
+ header_container.grid_columnconfigure(1, weight=1)
+
+ # Left side frame for logo and title
+ left_frame = ctk.CTkFrame(header_container, fg_color="transparent")
+ left_frame.grid(row=0, column=0, sticky="w", padx=30, pady=15)
+
+ # Frame for title components
+ title_frame = ctk.CTkFrame(left_frame, fg_color="transparent")
+ title_frame.pack(side="left")
+
+ # Main title "CS2" with accent color
+ main_title = ctk.CTkLabel(
+ title_frame,
+ text="CS2",
+ font=("Chivo", 28, "bold"),
+ text_color="#D5006D"
+ )
+ main_title.pack(side="left")
+
+ # Subtitle "TriggerBot" in white
+ sub_title = ctk.CTkLabel(
+ title_frame,
+ text="TriggerBot",
+ font=("Chivo", 28, "bold"),
+ text_color="#E0E0E0"
+ )
+ sub_title.pack(side="left", padx=(5, 0))
+
+ # Version label with smaller font and gray color
+ version_label = ctk.CTkLabel(
+ title_frame,
+ text=f"{ConfigManager.VERSION}",
+ font=("Gambetta", 12),
+ text_color="#6b7280"
+ )
+ version_label.pack(side="left", padx=(10, 0), pady=(8, 0))
+
+ # Right side frame for status and action buttons
+ right_frame = ctk.CTkFrame(header_container, fg_color="transparent")
+ right_frame.grid(row=0, column=2, sticky="e", padx=30, pady=15)
+
+ # Status indicator frame
+ self.status_frame = ctk.CTkFrame(right_frame, fg_color="transparent")
+ self.status_frame.pack(side="right", padx=(20, 0))
+
+ # Status dot indicating bot activity
+ status_dot = ctk.CTkFrame(
+ self.status_frame,
+ width=12,
+ height=12,
+ corner_radius=6,
+ fg_color="#ef4444"
+ )
+ status_dot.pack(side="left", pady=(0, 2))
+
+ # Status label showing "Inactive" or "Active"
+ self.status_label = ctk.CTkLabel(
+ self.status_frame,
+ text="Inactive",
+ font=("Chivo", 14, "bold"),
+ text_color="#ef4444"
+ )
+ self.status_label.pack(side="left", padx=(8, 0))
+
+ # Frame for social media buttons
+ social_frame = ctk.CTkFrame(right_frame, fg_color="transparent")
+ social_frame.pack(side="right")
+
+ # Load social media icons using CTkImage
+ try:
+ github_image = Image.open(Utility.resource_path('src/img/github_icon.png'))
+ self.github_ctk_image = ctk.CTkImage(light_image=github_image, dark_image=github_image, size=(24, 24))
+ except FileNotFoundError:
+ self.github_ctk_image = None
+
+ try:
+ telegram_image = Image.open(Utility.resource_path('src/img/telegram_icon.png'))
+ self.telegram_ctk_image = ctk.CTkImage(light_image=telegram_image, dark_image=telegram_image, size=(24, 24))
+ except FileNotFoundError:
+ self.telegram_ctk_image = None
- # Build tabs using functions from separate modules.
- init_home_tab(self)
- init_general_settings_tab(self)
- init_logs_tab(self)
- init_faq_tab(self)
-
- # Build the top section (header) with app name and icon buttons.
- self.top_layout = self.build_top_layout()
+ try:
+ boosty_image = Image.open(Utility.resource_path('src/img/boosty_icon.png'))
+ self.boosty_ctk_image = ctk.CTkImage(light_image=boosty_image, dark_image=boosty_image, size=(24, 24))
+ except FileNotFoundError:
+ self.boosty_ctk_image = None
+
+ # GitHub button with icon and link
+ github_btn = ctk.CTkButton(
+ social_frame,
+ text="GitHub",
+ image=self.github_ctk_image,
+ compound="left",
+ command=lambda: webbrowser.open("https://github.com/Jesewe/cs2-triggerbot"),
+ height=32,
+ corner_radius=16,
+ fg_color="#21262d",
+ hover_color="#30363d",
+ border_width=1,
+ border_color="#30363d",
+ font=("Chivo", 14)
+ )
+ github_btn.pack(side="left", padx=(0, 8))
+
+ # Telegram button with icon and link
+ telegram_btn = ctk.CTkButton(
+ social_frame,
+ text="Telegram",
+ image=self.telegram_ctk_image,
+ compound="left",
+ command=lambda: webbrowser.open("https://t.me/cs2_jesewe"),
+ height=32,
+ corner_radius=16,
+ fg_color="#0088cc",
+ hover_color="#006bb3",
+ font=("Chivo", 14)
+ )
+ telegram_btn.pack(side="left", padx=(0, 8))
+
+ # Boosty button with icon and link
+ boosty_btn = ctk.CTkButton(
+ social_frame,
+ text="Boosty",
+ image=self.boosty_ctk_image,
+ compound="left",
+ command=lambda: webbrowser.open("https://boosty.to/jesewe"),
+ height=32,
+ corner_radius=16,
+ fg_color="#ff6b35",
+ hover_color="#e55a2b",
+ font=("Chivo", 14)
+ )
+ boosty_btn.pack(side="left")
+
+ # Check for updates if running as an executable
+ if getattr(sys, 'frozen', False):
+ logger.info("Running from executable. Checking for updates...")
+ download_url = Utility.check_for_updates(ConfigManager.VERSION)
+ if download_url:
+ self.download_url = download_url
+ # Update button shown when a new version is available
+ update_btn = ctk.CTkButton(
+ social_frame,
+ text="Update Available",
+ command=self.handle_update,
+ width=80,
+ height=32,
+ corner_radius=16,
+ fg_color="#ef4444",
+ hover_color="#dc2626",
+ font=("Chivo", 14)
+ )
+ update_btn.pack(side="left", padx=(8, 0))
+ else:
+ logger.info("Running from source code. Auto-update disabled.")
- self.main_layout.addLayout(self.top_layout)
- self.main_layout.addWidget(self.tabs)
- container = QWidget()
- container.setLayout(self.main_layout)
- self.setCentralWidget(container)
+ def handle_update(self):
+ """Handle the update process with user confirmation."""
+ # Check if download URL exists
+ if not hasattr(self, 'download_url'):
+ messagebox.showerror("Error", "No update available.")
+ return
- # Initialize file watcher for configuration changes.
- self.init_config_watcher()
+ # Prompt user to confirm update
+ response = messagebox.askyesno("Update Available", "A new version is available. Are you ready to update?")
+ if response:
+ messagebox.showinfo("Updating", "Downloading update in background. You will be notified when the update is complete.")
+ # Start update in a separate thread
+ threading.Thread(target=self.download_and_update, args=(self.download_url,)).start()
- def apply_stylesheet(self, stylesheet_path):
- """Load and apply the stylesheet if available."""
+ def download_and_update(self, download_url):
+ """Download the new executable, create a .bat file to update and notify."""
try:
- with open(stylesheet_path, 'r') as f:
- self.setStyleSheet(f.read())
+ logger.info(f"Downloading update from {download_url}")
+ response = requests.get(download_url, stream=True)
+ response.raise_for_status()
+
+ # Define paths for current and temporary executables
+ current_exe = sys.executable
+ exe_name = os.path.basename(current_exe)
+ temp_exe = os.path.join(ConfigManager.UPDATE_DIRECTORY, "new_CS2.Triggerbot.exe")
+ bat_file = os.path.join(ConfigManager.UPDATE_DIRECTORY, "update.bat")
+
+ # Download the new executable
+ with open(temp_exe, 'wb') as f:
+ for chunk in response.iter_content(chunk_size=8192):
+ f.write(chunk)
+
+ logger.info("Update downloaded successfully")
+
+ # Create a batch file to handle the update process
+ with open(bat_file, 'w') as f:
+ f.write(f'''@echo off
+title CS2 TriggerBot Updater
+echo Updating CS2 TriggerBot...
+echo.
+echo Waiting for application to close...
+timeout /t 3 /nobreak >nul
+
+:WAIT_LOOP
+tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I /N "{exe_name}">NUL
+if "%ERRORLEVEL%"=="0" (
+ echo Application is still running, waiting...
+ timeout /t 2 /nobreak >nul
+ goto WAIT_LOOP
+)
+
+echo Backing up current version...
+if exist "{current_exe}.backup" del "{current_exe}.backup"
+move "{current_exe}" "{current_exe}.backup"
+
+echo Installing new version...
+move "{temp_exe}" "{current_exe}"
+
+echo Starting updated application...
+start "" "{current_exe}"
+
+echo Update completed successfully!
+timeout /t 3 /nobreak >nul
+
+echo Cleaning up...
+del "{current_exe}.backup" 2>nul
+del "%~f0" 2>nul
+''')
+
+ logger.info(f".bat file created at {bat_file}")
+
+ # Execute the batch file and close the application
+ subprocess.Popen(bat_file, shell=True)
+ self.root.quit()
except Exception as e:
- logger.error("Failed to load stylesheet: %s", e)
-
- def set_app_icon(self, icon_path):
- """Set the application icon if the file exists."""
- if os.path.exists(icon_path):
- self.setWindowIcon(QIcon(icon_path))
- else:
- logger.info("Icon not found at %s, skipping.", icon_path)
+ logger.error(f"Failed to update: {e}")
+ messagebox.showerror("Update Error", f"Failed to update: {str(e)}")
+
+ def create_main_content(self):
+ """Create the main content area with modern layout."""
+ # Main container for content and sidebar
+ main_container = ctk.CTkFrame(self.root, fg_color="transparent")
+ main_container.grid(row=1, column=0, sticky="nsew", padx=0, pady=0)
+ main_container.grid_columnconfigure(1, weight=1)
+ main_container.grid_rowconfigure(0, weight=1)
+
+ # Create sidebar navigation
+ self.create_sidebar(main_container)
+
+ # Content area frame
+ self.content_frame = ctk.CTkFrame(
+ main_container,
+ corner_radius=0,
+ fg_color=("#f8fafc", "#161b22")
+ )
+ self.content_frame.grid(row=0, column=1, sticky="nsew", padx=0, pady=0)
+
+ # Frames for each tab
+ self.dashboard_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
+ self.settings_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
+ self.logs_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
+ self.faq_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
+ self.supporters_frame = ctk.CTkFrame(self.content_frame, fg_color="transparent")
+
+ # Populate tab frames once during initialization
+ self.populate_dashboard()
+ self.populate_settings()
+ self.populate_logs()
+ self.populate_faq()
+ self.populate_supporters()
+
+ # Show dashboard as the default view
+ self.dashboard_frame.pack(fill="both", expand=True)
+ self.current_view = "dashboard"
+
+ def create_sidebar(self, parent):
+ """Create modern sidebar navigation."""
+ # Sidebar frame with fixed width
+ sidebar = ctk.CTkFrame(
+ parent,
+ width=280,
+ corner_radius=0,
+ fg_color=("#ffffff", "#0d1117")
+ )
+ sidebar.grid(row=0, column=0, sticky="nsew", padx=0, pady=0)
+ sidebar.grid_propagate(False)
+
+ # Navigation items with icons and labels
+ nav_items = [
+ ("Dashboard", "dashboard", "🏠"),
+ ("Settings", "settings", "⚙️"),
+ ("Logs", "logs", "📋"),
+ ("FAQ", "faq", "❓"),
+ ("Supporters", "supporters", "🤝")
+ ]
+
+ # Dictionary to store navigation buttons
+ self.nav_buttons = {}
+
+ # Add padding at the top of the sidebar
+ ctk.CTkFrame(sidebar, height=30, fg_color="transparent").pack(fill="x")
+
+ # Create navigation buttons
+ for name, key, icon in nav_items:
+ btn = ctk.CTkButton(
+ sidebar,
+ text=f"{icon} {name}",
+ command=lambda k=key: self.switch_view(k),
+ width=240,
+ height=50,
+ corner_radius=12,
+ fg_color="transparent",
+ hover_color=("#e5e7eb", "#21262d"),
+ text_color=("#374151", "#d1d5db"),
+ font=("Chivo", 16),
+ anchor="w"
+ )
+ btn.pack(pady=(0, 8), padx=20, fill="x")
+ self.nav_buttons[key] = btn
+
+ # Set the dashboard button as active by default
+ self.set_active_nav("dashboard")
+
+ def set_active_nav(self, active_key):
+ """Set the active navigation button with visual feedback."""
+ for key, btn in self.nav_buttons.items():
+ if key == active_key:
+ # Highlight the active button
+ btn.configure(
+ fg_color=("#D5006D", "#D5006D"),
+ text_color="#ffffff"
+ )
+ else:
+ # Reset inactive buttons to default style
+ btn.configure(
+ fg_color="transparent",
+ text_color=("#374151", "#d1d5db")
+ )
+
+ def switch_view(self, view_key):
+ """Switch between different views by showing the appropriate frame."""
+ # Avoid redundant switches
+ if self.current_view == view_key:
+ return
+ self.current_view = view_key
+ self.set_active_nav(view_key)
+
+ # Hide all frames
+ self.dashboard_frame.pack_forget()
+ self.settings_frame.pack_forget()
+ self.logs_frame.pack_forget()
+ self.faq_frame.pack_forget()
+ self.supporters_frame.pack_forget()
+
+ # Show the selected frame and update if necessary
+ if view_key == "dashboard":
+ self.dashboard_frame.pack(fill="both", expand=True)
+ elif view_key == "settings":
+ self.update_settings_fields()
+ self.settings_frame.pack(fill="both", expand=True)
+ elif view_key == "logs":
+ self.logs_frame.pack(fill="both", expand=True)
+ elif view_key == "faq":
+ self.faq_frame.pack(fill="both", expand=True)
+ elif view_key == "supporters":
+ self.supporters_frame.pack(fill="both", expand=True)
+
+ def populate_dashboard(self):
+ """Populate the dashboard frame with controls and stats."""
+ populate_dashboard(self, self.dashboard_frame)
+
+ def populate_settings(self):
+ """Populate the settings frame with configuration options."""
+ populate_settings(self, self.settings_frame)
+
+ def populate_logs(self):
+ """Populate the logs frame with log display."""
+ populate_logs(self, self.logs_frame)
+
+ def populate_faq(self):
+ """Populate the FAQ frame with questions and answers."""
+ populate_faq(self, self.faq_frame)
+
+ def populate_supporters(self):
+ """Populate the supporters frame with supporter data."""
+ populate_supporters(self, self.supporters_frame)
+
+ def update_settings_fields(self):
+ """Update the settings input fields with current configuration."""
+ # Retrieve current settings
+ settings = self.bot.config.get('Settings', {})
+ # Update trigger key field
+ self.trigger_key_entry.delete(0, 'end')
+ self.trigger_key_entry.insert(0, settings.get('TriggerKey', ''))
+ # Update toggle mode checkbox
+ self.toggle_mode_var.set(settings.get('ToggleMode', False))
+ # Update attack teammates checkbox
+ self.attack_teammates_var.set(settings.get('AttackOnTeammates', False))
+ # Update minimum delay field
+ self.min_delay_entry.delete(0, 'end')
+ self.min_delay_entry.insert(0, str(settings.get('ShotDelayMin', 0.01)))
+ # Update maximum delay field
+ self.max_delay_entry.delete(0, 'end')
+ self.max_delay_entry.insert(0, str(settings.get('ShotDelayMax', 0.03)))
+ # Update post-shot delay field
+ self.post_shot_delay_entry.delete(0, 'end')
+ self.post_shot_delay_entry.insert(0, str(settings.get('PostShotDelay', 0.1)))
def fetch_offsets_or_warn(self):
"""Attempt to fetch offsets; warn the user and return empty dictionaries on failure."""
@@ -88,126 +522,52 @@ def fetch_offsets_or_warn(self):
raise ValueError("Failed to fetch offsets from the server.")
return offsets, client_data
except Exception as e:
- QMessageBox.warning(self, "Offsets Fetch Error", str(e))
+ logger.error("Offsets fetch error: %s", e)
return {}, {}
- def build_top_layout(self):
- """Builds the top layout with the app title and icon buttons."""
- top_layout = QHBoxLayout()
-
- # Application title label.
- name_app = QLabel(f"CS2 TriggerBot {CS2TriggerBot.VERSION}")
- name_app.setStyleSheet("color: #D5006D; font-size: 22px; font-weight: bold;")
-
- # Create icon buttons.
- icon_layout = QHBoxLayout()
- icon_layout.setAlignment(Qt.AlignmentFlag.AlignRight)
- icon_layout.addWidget(self.create_icon_button('src/img/telegram_icon.png',
- "Join our Telegram channel",
- "https://t.me/cs2_jesewe"))
- icon_layout.addWidget(self.create_icon_button('src/img/github_icon.png',
- "Visit our GitHub repository",
- "https://github.com/Jesewe/cs2-triggerbot"))
-
- # Check for updates.
- update_url = Utility.check_for_updates(CS2TriggerBot.VERSION)
- if update_url:
- update_btn = self.create_icon_button('src/img/update_icon.png',
- "New update available! Click to download.",
- update_url,
- custom_style=("QPushButton { background-color: #333333; "
- "border-radius: 12px; border: 2px solid #D5006D; } "
- "QPushButton:hover { background-color: #444444; }"))
- icon_layout.addWidget(update_btn)
-
- # Assemble top layout.
- top_layout.addWidget(name_app)
- top_layout.addStretch()
- top_layout.addLayout(icon_layout)
- return top_layout
-
- def create_icon_button(self, relative_path, tooltip, url, custom_style=None):
- """
- Create a flat icon button that opens the provided URL when clicked.
- """
- btn = QPushButton()
- btn.setIcon(QIcon(Utility.resource_path(relative_path)))
- btn.setIconSize(QSize(24, 24))
- btn.setFlat(True)
- btn.setToolTip(tooltip)
- if custom_style:
- btn.setStyleSheet(custom_style)
- btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(url)))
- return btn
-
- def init_config_watcher(self):
- """
- Initializes a file watcher to monitor changes in the configuration file.
- Automatically updates the bot's configuration when the file changes.
- """
- try:
- event_handler = ConfigFileChangeHandler(self.bot)
- self.observer = Observer()
- self.observer.schedule(event_handler, path=ConfigManager.CONFIG_DIRECTORY, recursive=False)
- self.observer.start()
- logger.info("Config file watcher started successfully.")
- except Exception as e:
- logger.error("Failed to initialize config watcher: %s", e)
-
- def closeEvent(self, event):
- """
- Handles application close event to ensure resources are cleaned up:
- - Stops the file watcher.
- - Stops the bot if it is running.
- """
- try:
- if hasattr(self, 'observer'):
- self.observer.stop()
- self.observer.join()
- except Exception as e:
- logger.error("Error stopping observer: %s", e)
-
- if self.bot.is_running:
- self.bot.stop()
- if hasattr(self, 'bot_thread') and self.bot_thread is not None:
- self.bot_thread.join(timeout=2)
- if self.bot_thread.is_alive():
- logger.warning("Bot thread did not terminate cleanly.")
- self.bot_thread = None
- event.accept()
+ def update_bot_status(self, status, color):
+ """Update bot status in header and dashboard."""
+ # Update header status label
+ self.status_label.configure(text=status, text_color=color)
+
+ # Update header status dot color
+ for widget in self.status_frame.winfo_children():
+ if isinstance(widget, ctk.CTkFrame) and widget.cget("width") == 12:
+ widget.configure(fg_color=color)
+ break
+
+ # Update dashboard status label if it exists
+ if hasattr(self, 'bot_status_label'):
+ self.bot_status_label.configure(text=status, text_color=color)
def start_bot(self):
- """
- Starts the bot if it is not already running:
- - Ensures the game process (cs2.exe) is running.
- - Launches the bot in a separate thread.
- """
+ """Start the bot if it is not already running."""
+ # Check if bot is already active
if self.bot.is_running:
- QMessageBox.warning(self, "Bot Already Running", "The bot is already running.")
+ messagebox.showwarning("Bot Already Running", "The bot is already running.")
return
+ # Verify if the game is running
if not Utility.is_game_running():
- QMessageBox.critical(self, "Game Not Running", "Could not find cs2.exe process. Make sure the game is running.")
+ messagebox.showerror("Game Not Running", "Could not find cs2.exe process. Make sure the game is running.")
return
- # Clear the stop event to ensure the bot runs.
+ # Clear stop event and start bot in a new thread
self.bot.stop_event.clear()
self.bot_thread = threading.Thread(target=self.bot.start, daemon=True)
self.bot_thread.start()
- self.status_label.setText("Bot Status: Active")
- self.status_label.setStyleSheet("font-size: 16px; color: #008000; font-weight: bold;")
+ # Update UI to reflect active status
+ self.update_bot_status("Active", "#22c55e")
def stop_bot(self):
- """
- Stops the bot if it is currently running:
- - Signals the bot's stop event to terminate its main loop.
- - Waits for the bot's thread to terminate and updates the UI.
- """
+ """Stop the bot if it is currently running."""
+ # Check if bot is not running
if not self.bot.is_running:
- QMessageBox.warning(self, "Bot Not Started", "The bot is not running.")
+ messagebox.showwarning("Bot Not Started", "The bot is not running.")
return
+ # Stop the bot and wait for the thread to finish
self.bot.stop()
if hasattr(self, 'bot_thread') and self.bot_thread is not None:
self.bot_thread.join(timeout=2)
@@ -215,5 +575,340 @@ def stop_bot(self):
logger.warning("Bot thread did not terminate cleanly.")
self.bot_thread = None
- self.status_label.setText("Bot Status: Inactive")
- self.status_label.setStyleSheet("font-size: 16px; color: #FF5252; font-weight: bold;")
\ No newline at end of file
+ # Update UI to reflect inactive status
+ self.update_bot_status("Inactive", "#ef4444")
+
+ def save_settings(self, show_message=True):
+ """Save the configuration settings."""
+ try:
+ # Validate input fields
+ self.validate_inputs()
+
+ # Update configuration with current values
+ settings = self.bot.config['Settings']
+ settings['TriggerKey'] = self.trigger_key_entry.get().strip()
+ settings['ToggleMode'] = self.toggle_mode_var.get()
+ settings['AttackOnTeammates'] = self.attack_teammates_var.get()
+ settings['ShotDelayMin'] = float(self.min_delay_entry.get())
+ settings['ShotDelayMax'] = float(self.max_delay_entry.get())
+ settings['PostShotDelay'] = float(self.post_shot_delay_entry.get())
+
+ # Save and apply the updated configuration
+ ConfigManager.save_config(self.bot.config)
+ self.bot.update_config(self.bot.config)
+ if show_message:
+ messagebox.showinfo("Settings Saved", "Configuration has been successfully saved.")
+ except ValueError as e:
+ messagebox.showerror("Invalid Input", str(e))
+
+ def validate_inputs(self):
+ """Validate user input fields."""
+ # Check if trigger key is provided
+ trigger_key = self.trigger_key_entry.get().strip()
+ if not trigger_key:
+ raise ValueError("Trigger key cannot be empty.")
+
+ # Validate delay fields as numbers
+ try:
+ min_delay = float(self.min_delay_entry.get())
+ max_delay = float(self.max_delay_entry.get())
+ post_delay = float(self.post_shot_delay_entry.get())
+ except ValueError:
+ raise ValueError("Delay values must be valid numbers.")
+
+ # Ensure delays are non-negative and logical
+ if min_delay < 0 or max_delay < 0 or post_delay < 0:
+ raise ValueError("Delay values must be non-negative.")
+ if min_delay > max_delay:
+ raise ValueError("Minimum delay cannot be greater than maximum delay.")
+
+ def reset_to_defaults(self):
+ """Reset all settings to default values."""
+ # Retrieve default settings
+ defaults = ConfigManager.DEFAULT_CONFIG['Settings']
+
+ # Reset trigger key
+ self.trigger_key_entry.delete(0, 'end')
+ self.trigger_key_entry.insert(0, defaults.get('TriggerKey', ''))
+
+ # Reset toggle mode and attack teammates
+ self.toggle_mode_var.set(defaults.get('ToggleMode', False))
+ self.attack_teammates_var.set(defaults.get('AttackOnTeammates', False))
+
+ # Reset delay fields
+ self.min_delay_entry.delete(0, 'end')
+ self.min_delay_entry.insert(0, str(defaults.get('ShotDelayMin', 0.01)))
+ self.max_delay_entry.delete(0, 'end')
+ self.max_delay_entry.insert(0, str(defaults.get('ShotDelayMax', 0.03)))
+ self.post_shot_delay_entry.delete(0, 'end')
+ self.post_shot_delay_entry.insert(0, str(defaults.get('PostShotDelay', 0.1)))
+
+ # Save without showing a message
+ self.save_settings(show_message=False)
+ messagebox.showinfo("Settings Reset", "All settings have been reset to default values.")
+
+ def open_config_directory(self):
+ """Open the configuration directory in the file explorer."""
+ path = ConfigManager.CONFIG_DIRECTORY
+ if platform.system() == "Windows":
+ os.startfile(path)
+
+ def show_share_import_dialog(self):
+ """Show modern share/import dialog for configuration sharing."""
+ dialog = ctk.CTkToplevel(self.root)
+ dialog.title("Share/Import Settings")
+ dialog.geometry("600x500")
+ dialog.transient(self.root)
+ dialog.grab_set()
+ dialog.resizable(False, False)
+
+ # Center the dialog relative to the main window
+ self.root.update_idletasks()
+ root_x = self.root.winfo_rootx()
+ root_y = self.root.winfo_rooty()
+ root_w = self.root.winfo_width()
+ root_h = self.root.winfo_height()
+ dialog_w = 600
+ dialog_h = 500
+ x = root_x + (root_w // 2) - (dialog_w // 2)
+ y = root_y + (root_h // 2) - (dialog_h // 2)
+ dialog.geometry(f"{dialog_w}x{dialog_h}+{x}+{y}")
+
+ # Main frame for dialog content
+ main_frame = ctk.CTkFrame(dialog, fg_color="transparent")
+ main_frame.pack(fill="both", expand=True, padx=30, pady=30)
+
+ # Dialog title
+ ctk.CTkLabel(
+ main_frame,
+ text="🔗 Share/Import Configuration",
+ font=("Chivo", 24, "bold"),
+ text_color=("#1f2937", "#E0E0E0")
+ ).pack(pady=(0, 20))
+
+ # Description of dialog purpose
+ ctk.CTkLabel(
+ main_frame,
+ text="Generate a shareable code for your settings or import settings from a code",
+ font=("Gambetta", 14),
+ text_color=("#6b7280", "#9ca3af")
+ ).pack(pady=(0, 30))
+
+ # Text area for displaying or entering codes
+ text_frame = ctk.CTkFrame(
+ main_frame,
+ corner_radius=12,
+ fg_color=("#f8fafc", "#0d1117"),
+ border_width=1,
+ border_color=("#e5e7eb", "#30363d")
+ )
+ text_frame.pack(fill="both", expand=True, pady=(0, 30))
+
+ self.share_import_text = ctk.CTkTextbox(
+ text_frame,
+ corner_radius=12,
+ border_width=0,
+ state="disabled",
+ font=("Chivo", 12),
+ fg_color="transparent"
+ )
+ self.share_import_text.pack(fill="both", expand=True, padx=15, pady=15)
+
+ # Buttons frame for actions
+ buttons_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
+ buttons_frame.pack(fill="x")
+
+ # Generate code button
+ ctk.CTkButton(
+ buttons_frame,
+ text="📤 Generate Code",
+ command=self.export_settings,
+ width=140,
+ height=40,
+ corner_radius=10,
+ fg_color="#22c55e",
+ hover_color="#16a34a",
+ font=("Chivo", 14, "bold")
+ ).pack(side="left", padx=(0, 10))
+
+ # Import settings button
+ ctk.CTkButton(
+ buttons_frame,
+ text="📥 Import Settings",
+ command=lambda: self.import_settings(dialog),
+ width=140,
+ height=40,
+ corner_radius=10,
+ fg_color="#3b82f6",
+ hover_color="#2563eb",
+ font=("Chivo", 14, "bold")
+ ).pack(side="left", padx=(0, 10))
+
+ # Close dialog button
+ ctk.CTkButton(
+ buttons_frame,
+ text="❌ Close",
+ command=dialog.destroy,
+ width=100,
+ height=40,
+ corner_radius=10,
+ fg_color="#6b7280",
+ hover_color="#4b5563",
+ font=("Chivo", 14, "bold")
+ ).pack(side="right")
+
+ def export_settings(self):
+ """Export settings to a shareable code."""
+ # Collect current settings into a dictionary
+ settings = {
+ 'TriggerKey': self.trigger_key_entry.get(),
+ 'ToggleMode': self.toggle_mode_var.get(),
+ 'ShotDelayMin': float(self.min_delay_entry.get()),
+ 'ShotDelayMax': float(self.max_delay_entry.get()),
+ 'PostShotDelay': float(self.post_shot_delay_entry.get()),
+ 'AttackOnTeammates': self.attack_teammates_var.get()
+ }
+
+ # Serialize, compress, and encode the settings
+ json_bytes = orjson.dumps(settings)
+ compressed = zlib.compress(json_bytes)
+ encoded = base64.b64encode(compressed).decode()
+ code = f"TB-{encoded}"
+
+ # Display and copy the code to clipboard
+ self.share_import_text.configure(state="normal")
+ self.share_import_text.delete("1.0", "end")
+ self.share_import_text.insert("1.0", code)
+ self.share_import_text.configure(state="disabled")
+ self.root.clipboard_clear()
+ self.root.clipboard_append(code)
+
+ messagebox.showinfo("Code Generated", "Settings code has been generated and copied to clipboard!")
+
+ def import_settings(self, dialog):
+ """Import settings from a provided code."""
+ # Enable text box to read the code
+ self.share_import_text.configure(state="normal")
+ code = self.share_import_text.get("1.0", "end-1c").strip()
+ self.share_import_text.configure(state="disabled")
+
+ # Validate code prefix
+ if not code.startswith("TB-"):
+ messagebox.showerror("Invalid Code", "Invalid code format. Must start with 'TB-'")
+ return
+
+ try:
+ # Decode, decompress, and deserialize the settings
+ encoded = code[3:]
+ compressed = base64.b64decode(encoded)
+ json_bytes = zlib.decompress(compressed)
+ settings = orjson.loads(json_bytes)
+
+ # Update UI fields with imported settings
+ self.trigger_key_entry.delete(0, 'end')
+ self.trigger_key_entry.insert(0, settings.get('TriggerKey', ''))
+ self.toggle_mode_var.set(settings.get('ToggleMode', False))
+ self.attack_teammates_var.set(settings.get('AttackOnTeammates', False))
+ self.min_delay_entry.delete(0, 'end')
+ self.min_delay_entry.insert(0, str(settings.get('ShotDelayMin', 0.01)))
+ self.max_delay_entry.delete(0, 'end')
+ self.max_delay_entry.insert(0, str(settings.get('ShotDelayMax', 0.03)))
+ self.post_shot_delay_entry.delete(0, 'end')
+ self.post_shot_delay_entry.insert(0, str(settings.get('PostShotDelay', 0.1)))
+
+ # Save settings and close dialog
+ self.save_settings(show_message=False)
+ dialog.destroy()
+ messagebox.showinfo("Import Successful", "Settings have been imported and saved successfully!")
+ except Exception as e:
+ messagebox.showerror("Import Error", f"Error importing settings: {str(e)}")
+
+ def init_config_watcher(self):
+ """Initialize file watcher for configuration changes."""
+ try:
+ # Set up a watcher for config file changes
+ event_handler = ConfigFileChangeHandler(self.bot)
+ self.observer = Observer()
+ self.observer.schedule(event_handler, path=ConfigManager.CONFIG_DIRECTORY, recursive=False)
+ self.observer.start()
+ logger.info("Config file watcher started successfully.")
+ except Exception as e:
+ logger.error("Failed to initialize config watcher: %s", e)
+
+ def start_log_timer(self):
+ """Start timer for updating logs in a separate thread."""
+ def update_logs():
+ logger = Logger.get_logger()
+ while True:
+ try:
+ # Update logs if the widget exists and log file is present
+ if hasattr(self, 'log_text') and os.path.exists(Logger.LOG_FILE):
+ file_size = os.path.getsize(Logger.LOG_FILE)
+ # Reset position if log file is truncated
+ if file_size < self.last_log_position:
+ self.last_log_position = 0
+ if self.last_log_position < file_size:
+ with open(Logger.LOG_FILE, 'r', encoding='utf-8') as log_file:
+ log_file.seek(self.last_log_position)
+ new_logs = log_file.read()
+ self.last_log_position = log_file.tell()
+ if new_logs:
+ self.root.after(0, lambda logs=new_logs: self.update_log_display(logs))
+ except Exception as e:
+ logger.error(f"Error in log update thread: {e}")
+ time.sleep(1)
+
+ # Start log update thread
+ self.log_timer = threading.Thread(target=update_logs, daemon=True)
+ self.log_timer.start()
+
+ def update_log_display(self, new_logs):
+ """Update the log display with new logs, limiting lines for performance."""
+ logger = Logger.get_logger()
+ try:
+ # Ensure log widget exists and is valid
+ if hasattr(self, 'log_text') and self.log_text.winfo_exists():
+ self.log_text.configure(state="normal")
+ self.log_text.insert("end", new_logs)
+ self.log_text.see("end")
+
+ # Limit log lines to 1000 for performance
+ max_lines = 1000
+ current_text = self.log_text.get("1.0", "end-1c")
+ lines = current_text.splitlines()
+ if len(lines) > max_lines:
+ excess_lines = len(lines) - max_lines
+ delete_to = f"{excess_lines + 1}.0"
+ self.log_text.delete("1.0", delete_to)
+
+ self.log_text.configure(state="disabled")
+ except Exception as e:
+ logger.error(f"Error updating log display: {e}")
+
+ def run(self):
+ """Start the application main loop."""
+ self.root.mainloop()
+
+ def on_closing(self):
+ """Handle window close event by cleaning up resources."""
+ self.cleanup()
+ self.root.destroy()
+
+ def cleanup(self):
+ """Cleanup resources before closing the application."""
+ try:
+ # Stop the file watcher if it exists
+ if hasattr(self, 'observer') and self.observer:
+ self.observer.stop()
+ self.observer.join()
+ except Exception as e:
+ logger.error("Error stopping observer: %s", e)
+
+ # Stop the bot if it’s running
+ if self.bot.is_running:
+ self.bot.stop()
+ if hasattr(self, 'bot_thread') and self.bot_thread is not None:
+ self.bot_thread.join(timeout=2)
+ if self.bot_thread.is_alive():
+ logger.warning("Bot thread did not terminate cleanly.")
+ self.bot_thread = None
\ No newline at end of file
diff --git a/gui/supporters_tab.py b/gui/supporters_tab.py
new file mode 100644
index 0000000..555705f
--- /dev/null
+++ b/gui/supporters_tab.py
@@ -0,0 +1,240 @@
+import customtkinter as ctk
+import orjson
+import threading
+import requests
+from classes.logger import Logger
+from classes.utility import Utility
+
+# Cache the logger instance
+logger = Logger.get_logger()
+
+def populate_supporters(main_window, frame):
+ """Populate the Supporters tab with data from a JSON file."""
+ # Main container
+ main_container = ctk.CTkFrame(frame, fg_color="transparent")
+ main_container.pack(fill="both", expand=True, padx=24, pady=24)
+
+ # Scrollable container
+ supporters_container = ctk.CTkScrollableFrame(
+ main_container, fg_color="transparent",
+ scrollbar_button_color=("#CBD5E1", "#475569"),
+ scrollbar_button_hover_color=("#94A3B8", "#64748B")
+ )
+ supporters_container.pack(fill="both", expand=True)
+
+ # Hero section
+ hero_frame = ctk.CTkFrame(
+ supporters_container, corner_radius=20, fg_color=("#FFFFFF", "#0F172A"),
+ border_width=2, border_color=("#E2E8F0", "#1E293B")
+ )
+ hero_frame.pack(fill="x", pady=(0, 48), padx=24)
+
+ # Hero content
+ hero_content = ctk.CTkFrame(hero_frame, fg_color="transparent")
+ hero_content.pack(fill="x", padx=48, pady=48)
+
+ # Title and subtitle
+ ctk.CTkLabel(
+ hero_content, text="🤝 Project Supporters", font=("Chivo", 48, "bold"),
+ text_color=("#0F172A", "#F8FAFC")
+ ).pack(anchor="w")
+ ctk.CTkLabel(
+ hero_content, text="Celebrating our incredible community members who fuel this project's growth",
+ font=("Gambetta", 20), text_color=("#475569", "#CBD5E1"), wraplength=800
+ ).pack(anchor="w", pady=(20, 0))
+
+ # Stats frame
+ stats_frame = ctk.CTkFrame(hero_content, fg_color="transparent")
+ stats_frame.pack(fill="x", pady=(40, 0))
+
+ # Content frame
+ content_frame = ctk.CTkFrame(supporters_container, fg_color="transparent")
+ content_frame.pack(fill="x", padx=24)
+
+ # Loading container
+ loading_container = ctk.CTkFrame(
+ content_frame, corner_radius=16, fg_color=("#FFFFFF", "#1E293B"),
+ border_width=1, border_color=("#E2E8F0", "#334155")
+ )
+ loading_container.pack(pady=24)
+
+ # Loading content
+ loading_content = ctk.CTkFrame(loading_container, fg_color="transparent")
+ loading_content.pack(padx=48, pady=36)
+
+ # Loading indicator and message
+ ctk.CTkFrame(
+ loading_content, width=48, height=48, corner_radius=24,
+ fg_color=("#3B82F6", "#60A5FA")
+ ).pack()
+ ctk.CTkLabel(
+ loading_content, text="Loading supporters data...", font=("Gambetta", 18),
+ text_color=("#64748B", "#94A3B8")
+ ).pack(pady=(20, 0))
+
+ # Fetch supporter data in a background thread
+ def fetch_supporters():
+ try:
+ # Fetch JSON data from GitHub with a timeout
+ response = requests.get('https://raw.githubusercontent.com/Jesewe/cs2-triggerbot/refs/heads/main/src/supporters.json', timeout=10)
+ response.raise_for_status()
+ data = orjson.loads(response.content)
+ main_window.root.after(0, lambda: update_supporters_ui(data, loading_container, stats_frame))
+ except requests.exceptions.RequestException as e:
+ main_window.root.after(0, lambda: show_error(loading_container, f"Failed to fetch supporters data: {e}"))
+ logger.error(f"Failed to fetch supporters data: {e}")
+ except orjson.JSONDecodeError as e:
+ main_window.root.after(0, lambda: show_error(loading_container, "Invalid JSON data received"))
+ logger.error(f"Invalid JSON data: {e}")
+ except Exception as e:
+ main_window.root.after(0, lambda: show_error(loading_container, str(e)))
+ logger.error(f"Unexpected error: {e}")
+
+ def update_supporters_ui(data, loading_container, stats_frame):
+ """Update the UI with fetched supporter data."""
+ # Remove loading container
+ loading_container.destroy()
+
+ total_supporters = 0
+ sections = [
+ ("boosty", "early_access", "Early Access Tier", "#F59E0B", "#FEF3C7", "#92400E", "🚀 Premium members with early access to new features"),
+ ("boosty", "supporter", "Community Supporters", "#3B82F6", "#DBEAFE", "#1E40AF", "💙 Valued community members supporting development")
+ ]
+
+ sections_created = 0
+ for platform, key, title, color1, color2, color3, desc in sections:
+ if platform in data and key in data[platform]:
+ create_section(content_frame, title, data[platform][key], color1, color2, color3, desc)
+ total_supporters += len(data[platform][key])
+ sections_created += 1
+
+ # Update stats display with totals
+ create_stats_display(stats_frame, total_supporters, sections_created)
+
+ def create_stats_display(stats_frame, total_supporters, total_tiers):
+ """Create statistics display in hero section."""
+ # Clear existing stats widgets
+ for widget in stats_frame.winfo_children():
+ widget.destroy()
+
+ # Stats container
+ container = ctk.CTkFrame(stats_frame, fg_color="transparent")
+ container.pack(fill="x", pady=8)
+
+ # Card configuration
+ card_config = {'corner_radius': 16, 'width': 200, 'height': 120, 'border_width': 2}
+
+ # Theme configurations
+ themes = [
+ {
+ 'fg_color': ("#E0F2FE", "#0F172A"),
+ 'border_color': ("#0EA5E9", "#0284C7"),
+ 'number_color': ("#0C4A6E", "#38BDF8"),
+ 'label_color': ("#0369A1", "#0EA5E9"),
+ 'value': total_supporters,
+ 'label': "Total Supporters"
+ },
+ {
+ 'fg_color': ("#F0FDF4", "#0F172A"),
+ 'border_color': ("#22C55E", "#16A34A"),
+ 'number_color': ("#15803D", "#4ADE80"),
+ 'label_color': ("#16A34A", "#22C55E"),
+ 'value': total_tiers,
+ 'label': "Support Tiers"
+ }
+ ]
+
+ # Create stat cards
+ for i, theme in enumerate(themes):
+ # Card frame
+ card = ctk.CTkFrame(container, fg_color=theme['fg_color'], border_color=theme['border_color'], **card_config)
+ card.pack(side="left", padx=(0, 32 if i == 0 else 0))
+ card.pack_propagate(False)
+
+ # Content frame
+ content = ctk.CTkFrame(card, fg_color="transparent")
+ content.pack(expand=True, fill="both", padx=16, pady=16)
+
+ # Value label
+ ctk.CTkLabel(content, text=f"{theme['value']:,}", font=("Chivo", 36, "bold"),
+ text_color=theme['number_color']).pack(expand=True, pady=(8, 0))
+
+ # Description label
+ ctk.CTkLabel(content, text=theme['label'], font=("Gambetta", 15, "normal"),
+ text_color=theme['label_color']).pack(pady=(0, 8))
+
+ def create_section(container, title, usernames, accent_color, bg_color, text_color, description):
+ """Create a section for a specific supporter tier."""
+ # Section wrapper
+ section = ctk.CTkFrame(container, fg_color="transparent")
+ section.pack(fill="x", pady=(0, 48))
+
+ # Header frame
+ header = ctk.CTkFrame(section, corner_radius=16, fg_color=("#FFFFFF", "#1E293B"),
+ border_width=2, border_color=("#F1F5F9", "#334155"))
+ header.pack(fill="x", pady=(0, 32))
+
+ # Header content
+ content = ctk.CTkFrame(header, fg_color="transparent")
+ content.pack(fill="x", padx=40, pady=32)
+
+ # Title and description
+ ctk.CTkFrame(content, height=4, width=100, corner_radius=2, fg_color=accent_color).pack(anchor="w", pady=(0, 16))
+ ctk.CTkLabel(content, text=title, font=("Chivo", 32, "bold"), text_color=("#0F172A", "#F8FAFC")).pack(anchor="w")
+ ctk.CTkLabel(content, text=description, font=("Gambetta", 18), text_color=("#475569", "#CBD5E1")).pack(anchor="w", pady=(12, 0))
+
+ # Member count badge
+ badge = ctk.CTkFrame(content, corner_radius=24, fg_color=bg_color, height=40, border_width=1, border_color=accent_color)
+ badge.pack(anchor="w", pady=(20, 0))
+ ctk.CTkLabel(badge, text=f"{len(usernames)} {'member' if len(usernames) == 1 else 'members'}",
+ font=("Chivo", 16, "bold"), text_color=text_color).pack(padx=20, pady=8)
+
+ # Supporter usernames grid
+ if usernames:
+ grid = ctk.CTkFrame(section, fg_color="transparent")
+ grid.pack(fill="x")
+ columns = min(4, max(2, len(usernames)))
+
+ for i, username in enumerate(usernames):
+ card = ctk.CTkFrame(grid, corner_radius=12, fg_color=("#FFFFFF", "#1E293B"),
+ border_width=1, border_color=("#E2E8F0", "#475569"), height=70)
+ card.grid(row=i // columns, column=i % columns, padx=(0 if i % columns == 0 else 12, 0 if i % columns == columns - 1 else 12),
+ pady=(0, 16), sticky="ew")
+ card.grid_propagate(False)
+ for c in range(columns):
+ grid.grid_columnconfigure(c, weight=1)
+
+ card_content = ctk.CTkFrame(card, fg_color="transparent")
+ card_content.pack(fill="both", expand=True, padx=24, pady=20)
+ ctk.CTkFrame(card_content, width=12, height=12, corner_radius=6, fg_color=accent_color).pack(side="left")
+ ctk.CTkLabel(card_content, text=username, font=("Chivo", 16, "bold"),
+ text_color=("#1E293B", "#F1F5F9")).pack(side="left", padx=(16, 0))
+
+ def show_error(loading_container, error_msg):
+ """Display an error message if data fetch fails."""
+ loading_container.destroy()
+
+ # Error frame
+ error_frame = ctk.CTkFrame(content_frame, corner_radius=16, fg_color=("#FEF2F2", "#1F1715"),
+ border_width=2, border_color=("#FCA5A5", "#7F1D1D"))
+ error_frame.pack(pady=24)
+
+ # Error content
+ content = ctk.CTkFrame(error_frame, fg_color="transparent")
+ content.pack(padx=48, pady=36)
+
+ # Error icon
+ icon = ctk.CTkFrame(content, width=56, height=56, corner_radius=28, fg_color=("#DC2626", "#7F1D1D"))
+ icon.pack()
+ ctk.CTkLabel(icon, text="✕", font=("Chivo", 24, "bold"), text_color=("#FFFFFF", "#FFFFFF")).pack(expand=True)
+
+ # Error message
+ ctk.CTkLabel(content, text="Failed to Load Data", font=("Chivo", 22, "bold"),
+ text_color=("#DC2626", "#F87171")).pack(pady=(20, 8))
+ ctk.CTkLabel(content, text=error_msg, font=("Gambetta", 16), text_color=("#991B1B", "#EF4444"),
+ wraplength=500).pack()
+ ctk.CTkLabel(content, text="Please check your internet connection and try again",
+ font=("Gambetta", 14), text_color=("#B91C1C", "#FCA5A5")).pack(pady=(12, 0))
+
+ # Start fetching supporters data
+ threading.Thread(target=fetch_supporters, daemon=True).start()
\ No newline at end of file
diff --git a/main.py b/main.py
index 20817aa..3320ab3 100644
--- a/main.py
+++ b/main.py
@@ -1,27 +1,28 @@
import sys
-from PyQt6.QtWidgets import QApplication
from classes.logger import Logger
from gui.main_window import MainWindow
-from classes.trigger_bot import CS2TriggerBot
+from classes.config_manager import ConfigManager
def main():
# Set up logging for the application.
Logger.setup_logging()
logger = Logger.get_logger()
- # Initialize the QApplication. If an instance already exists, use it; otherwise, create a new one.
- app = QApplication.instance() or QApplication(sys.argv)
-
# Log the loaded version.
- logger.info("Loaded version: %s", CS2TriggerBot.VERSION)
-
- # Create and display the main application window.
- window = MainWindow()
- window.show()
+ logger.info("Loaded version: %s", ConfigManager.VERSION)
- # Start the application event loop and exit with the returned status.
- sys.exit(app.exec())
+ try:
+ # Create and run the main application window.
+ window = MainWindow()
+ window.run()
+ except KeyboardInterrupt:
+ logger.info("Application interrupted by user")
+ except Exception as e:
+ logger.error("Unexpected error: %s", e)
+ sys.exit(1)
+ finally:
+ logger.info("Application shutting down")
if __name__ == "__main__":
main()
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 8f946db..ce3b211 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,11 +3,12 @@ packaging==25.0
psutil==7.0.0
pymem==1.14.0
pynput==1.8.1
-PyQt6==6.9.0
-PyQt6_sip==13.10.0
python_dateutil==2.9.0.post0
pywin32==310
-Requests==2.32.4
+requests==2.32.4
watchdog==6.0.0
PyGetWindow==0.0.9
-orjson==3.10.18
\ No newline at end of file
+orjson==3.10.18
+customtkinter==5.2.2
+pillow==11.2.1
+playsound==1.3.0
\ No newline at end of file
diff --git a/src/fonts/Chivo-Bold.ttf b/src/fonts/Chivo-Bold.ttf
new file mode 100644
index 0000000..5f57bd7
Binary files /dev/null and b/src/fonts/Chivo-Bold.ttf differ
diff --git a/src/fonts/Chivo-Regular.ttf b/src/fonts/Chivo-Regular.ttf
new file mode 100644
index 0000000..4eea4f1
Binary files /dev/null and b/src/fonts/Chivo-Regular.ttf differ
diff --git a/src/fonts/Gambetta-Bold.ttf b/src/fonts/Gambetta-Bold.ttf
new file mode 100644
index 0000000..666c3a8
Binary files /dev/null and b/src/fonts/Gambetta-Bold.ttf differ
diff --git a/src/fonts/Gambetta-Regular.ttf b/src/fonts/Gambetta-Regular.ttf
new file mode 100644
index 0000000..13abacf
Binary files /dev/null and b/src/fonts/Gambetta-Regular.ttf differ
diff --git a/src/img/background.bmp b/src/img/background.bmp
new file mode 100644
index 0000000..4f1f81a
Binary files /dev/null and b/src/img/background.bmp differ
diff --git a/src/img/background.png b/src/img/background.png
new file mode 100644
index 0000000..8cf32a3
Binary files /dev/null and b/src/img/background.png differ
diff --git a/src/img/boosty_icon.png b/src/img/boosty_icon.png
new file mode 100644
index 0000000..d8496ee
Binary files /dev/null and b/src/img/boosty_icon.png differ
diff --git a/src/img/icon.bmp b/src/img/icon.bmp
new file mode 100644
index 0000000..1e24d13
Binary files /dev/null and b/src/img/icon.bmp differ
diff --git a/src/supporters.json b/src/supporters.json
index a994684..decf59a 100644
--- a/src/supporters.json
+++ b/src/supporters.json
@@ -8,4 +8,4 @@
"SedoySadist"
]
}
-}
+}
\ No newline at end of file
diff --git a/version.txt b/version.txt
index 4a7e2ca..fcd0e39 100644
--- a/version.txt
+++ b/version.txt
@@ -3,8 +3,8 @@
VSVersionInfo(
ffi=FixedFileInfo(
- filevers=(1, 2, 4, 4),
- prodvers=(1, 2, 4, 4),
+ filevers=(1, 2, 5, 0),
+ prodvers=(1, 2, 5, 0),
mask=0x3f,
flags=0x0,
OS=0x4,
@@ -20,12 +20,12 @@ VSVersionInfo(
[
StringStruct(u'CompanyName', u'Jesewe'),
StringStruct(u'FileDescription', u'CS2 Triggerbot'),
- StringStruct(u'FileVersion', u'1.2.4.4'),
+ StringStruct(u'FileVersion', u'1.2.5.0'),
StringStruct(u'InternalName', u'CS2 Triggerbot'),
StringStruct(u'LegalCopyright', u'(C) 2025 Jesewe. MIT License'),
- StringStruct(u'OriginalFilename', u'CS2 Triggerbot.exe'),
+ StringStruct(u'OriginalFilename', u'CS2.Triggerbot.exe'),
StringStruct(u'ProductName', u'CS2 Triggerbot'),
- StringStruct(u'ProductVersion', u'1.2.4.4')
+ StringStruct(u'ProductVersion', u'1.2.5.0')
]
)
]