Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# Moonstone
A desktop app for `bol-van/zapret`
# Sakura Flow 🌸 (Moonstone Fork)
An optimized desktop tray application for managing [zapret](https://github.com) on Windows.

![alt text](images/interface.png)
![Interface](images/interface.png)

## Key Enhancements in Sakura Flow:
- **Redesigned UI**: Modern "Sakura" dark-pink theme for the system tray menu.
- **Improved UX**: The menu now opens with both **Left-Click (LMB)** and Right-Click (RMB).
- **Portable Mode Fix**: Improved path logic in `config.py`. The compiled `.exe` now correctly locates `zapret` and `icons` folders when placed in the same directory.
- **Enhanced Gaming Support**: Automatic UDP port injection in `service.py` for stable connectivity in games (Rocket League, etc.) with low ping.
- **DNS Flow**: Quick access to Windows Network Settings directly from the menu.

## Quick Start

### Running in Debug Mode
```bash
python -m src.main
```
**Note:** Requires administrator privileges.

### Building
```bash
pyinstaller --onedir --noconsole --name Moonstone --add-data "icons;icons" --add-data "zapret;zapret" --icon=icons/moonstone.ico --version-file=version.py src/main.py
```
pyinstaller --noconfirm --onefile --windowed --uac-admin --icon "icons/moonstone.ico" --name "SakuraFlow" src/main.py

For detailed information on development, debugging, and building, see [DEVELOPMENT.md](DEVELOPMENT.md).
```
Binary file modified icons/check.ico
Binary file not shown.
Binary file modified icons/check.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified icons/moonstone.ico
Binary file not shown.
Binary file modified icons/moonstone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/interface.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
"""Moonstone - Desktop app for bol-van/zapret."""

"""Sakura Flow - Optimized desktop app for bol-van/zapret."""
Binary file added src/__pycache__/__init__.cpython-313.pyc
Binary file not shown.
Binary file added src/__pycache__/admin.cpython-313.pyc
Binary file not shown.
Binary file added src/__pycache__/autostart.cpython-313.pyc
Binary file not shown.
Binary file added src/__pycache__/config.cpython-313.pyc
Binary file not shown.
Binary file added src/__pycache__/service.cpython-313.pyc
Binary file not shown.
Binary file added src/__pycache__/state.cpython-313.pyc
Binary file not shown.
Binary file added src/__pycache__/ui.cpython-313.pyc
Binary file not shown.
Binary file added src/__pycache__/updater.cpython-313.pyc
Binary file not shown.
25 changes: 12 additions & 13 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
"""Configuration constants for Moonstone application."""
"""Configuration constants for Sakura Flow application."""
import sys
import os
from pathlib import Path

# Service and Task names
SERVICE_NAME = "MoonstoneZapret"
TASK_NAME = "MoonstoneAutostart"
# Названия
SERVICE_NAME = "SakuraFlowService"
TASK_NAME = "SakuraFlowAutostart"

# Base directory detection (works for both frozen and development)
# КОРРЕКТНОЕ ОПРЕДЕЛЕНИЕ ПУТИ
if getattr(sys, 'frozen', False):
BASE_DIR = Path(sys._MEIPASS)
# Если запущен EXE, берем путь к папке, где лежит сам EXE
BASE_DIR = Path(sys.executable).resolve().parent
else:
# Если запущен скрипт, берем корень проекта
BASE_DIR = Path(__file__).resolve().parent.parent

# Paths
# Пути к файлам (теперь они будут искаться рядом с SakuraFlow.exe)
ICON_PATH = BASE_DIR / "icons" / "moonstone.ico"
CHECK_ICON_PATH = BASE_DIR / "icons" / "check.ico"
BAT_DIR = BASE_DIR / "zapret"
BUNDLED_DIR = BAT_DIR / "bundled"
BACKUP_DIR = BAT_DIR / "bundled_backup"
LOG_FILE = BASE_DIR / "moonstone.log"
STATE_FILE = BASE_DIR / "moonstone_state.json"
GITHUB_RELEASES_API = "https://api.github.com/repos/bol-van/zapret/releases/latest"

# Encoding
ENCODING = "cp866"

# Кодировка для Windows батников
ENCODING = "cp866"
33 changes: 16 additions & 17 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
"""Main entry point for Moonstone application."""
"""Main entry point for Sakura Flow application."""
import sys
import logging
from pathlib import Path

# Handle both direct execution and module execution
# Обработка путей для запуска
if __name__ == "__main__":
# Running as script - ensure parent directory is in path
file_path = Path(__file__).resolve()
parent_dir = file_path.parent.parent
if str(parent_dir) not in sys.path:
sys.path.insert(0, str(parent_dir))
# Force package initialization by importing the package
try:
import src
sys.modules['src'] = src
except:
pass
# Use absolute imports
from src import admin, ui, config
else:
# Running as module - use relative imports
from . import admin, ui, config

# Настройка логирования
Expand All @@ -31,23 +27,26 @@
encoding='utf-8'
)


def main():
"""Main entry point."""
logging.info("Начало выполнения скрипта")
"""Точка входа в приложение."""
logging.info("Запуск Sakura Flow")

# Check for admin privileges
# Проверка прав администратора
if not admin.is_admin():
logging.info("Требуются права администратора, перезапуск...")
logging.info("Запрос прав администратора...")
admin.run_as_admin()

logging.info("Вызов функции main()")

# Get batch files
bat_files = list(config.BAT_DIR.glob("*.bat"))
# --- ВОТ ТУТ ИЗМЕНЕНИЕ ---
# Получаем все .bat файлы, кроме service.bat (чтобы он не мозолил глаза в меню)
# Берем все .bat, кроме вспомогательных service.bat и самого general.bat
bat_files = [
f for f in config.BAT_DIR.glob("*.bat")
if f.name.lower() not in ["service.bat", "general.bat"]
]


# Create and run tray application
# Запуск интерфейса
sys.exit(ui.create_tray_app(bat_files))

if __name__ == "__main__":
main()
main()
172 changes: 54 additions & 118 deletions src/service.py
Original file line number Diff line number Diff line change
@@ -1,172 +1,108 @@
"""Windows service management functions."""
"""Windows service management functions for Sakura Flow."""
import subprocess
import re
import sys
import logging
from pathlib import Path

# Handle both relative and absolute imports
try:
from .config import SERVICE_NAME, BAT_DIR, ENCODING
except ImportError:
from src.config import SERVICE_NAME, BAT_DIR, ENCODING


def run_cmd(cmd):
"""Execute a shell command and log the results."""
"""Выполнение команды оболочки."""
logging.info(f"Выполнение команды: {cmd}")
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding=ENCODING)
if result.stdout:
logging.info(f"Вывод команды: {result.stdout.strip()}")
if result.stderr:
logging.error(f"Ошибка команды: {result.stderr.strip()}")
return result
except Exception as e:
logging.error(f"Исключение при выполнении команды: {e}")
logging.error(f"Ошибка команды: {e}")
return None


def service_exists():
"""Check if the service exists."""
"""Проверка существования службы."""
result = run_cmd(f'sc.exe query "{SERVICE_NAME}"')
if result and (SERVICE_NAME in result.stdout):
logging.info(f"Служба '{SERVICE_NAME}' существует.")
return True
logging.info(f"Служба '{SERVICE_NAME}' не существует.")
return False

return result and (SERVICE_NAME in result.stdout)

def get_service_display_name():
"""Get the display name of the service."""
if not service_exists():
return None
"""Получение имени службы."""
if not service_exists(): return None
result = run_cmd(f'sc.exe qc "{SERVICE_NAME}"')
if result and result.returncode == 0:
match = re.search(r'DISPLAY_NAME\s*:\s*(.+)', result.stdout)
if match:
display_name = match.group(1).strip()
logging.info(f"Получено отображаемое имя службы: {display_name}")
return display_name
logging.error("Не удалось получить отображаемое имя службы")
if match: return match.group(1).strip()
return None


def parse_bat_file(batch_path):
"""Parse a batch file to extract executable and arguments."""
logging.info(f"Чтение .bat файла: {batch_path}")
if not batch_path.exists():
logging.error(f"Файл .bat не найден: {batch_path}")
sys.exit(f"Batch file not found: {batch_path}")
try:
with open(batch_path, 'r', encoding=ENCODING) as f:
bat_content = f.read()
except Exception as e:
logging.error(f"Ошибка при чтении .bat файла: {e}")
sys.exit(f"Failed to read batch file: {e}")

bin_match = re.search(r'set\s+"?BIN=%~dp0([^"\s]*)"?', bat_content)
lists_match = re.search(r'set\s+"?LISTS=%~dp0([^"\s]*)"?', bat_content)
configs_match = re.search(r'set\s+"?CONFIGS=%~dp0([^"\s]*)"?', bat_content)
bin_path = Path(batch_path.parent / (bin_match.group(1) if bin_match else "bundled"))
lists_path = Path(batch_path.parent / (lists_match.group(1) if lists_match else "lists"))
configs_path = Path(batch_path.parent / (configs_match.group(1) if configs_match else "configs"))
logging.info(f"BIN путь: {bin_path}")
logging.info(f"LISTS путь: {lists_path}")
logging.info(f"CONFIGS путь: {configs_path}")

"""Парсинг .bat файла с подстановкой портов Rocket League."""
logging.info(f"Разбор стратегии: {batch_path}")
with open(batch_path, 'r', encoding=ENCODING) as f:
bat_content = f.read()

base_zapret = batch_path.parent
bin_dir = base_zapret / "bin"
lists_dir = base_zapret / "lists"
configs_dir = base_zapret / "configs"

# 1. Вшиваем порты Rocket League
game_ports = "1-65535"
bat_content = bat_content.replace("%GameFilter%", game_ports)

# 2. Извлекаем команду winws.exe
start_match = re.search(r'start\s+"[^"]*"\s+/min\s+"([^"]+)"\s+(.+)', bat_content, re.DOTALL)
if not start_match:
logging.error("Не удалось разобрать команду winws.exe из .bat файла")
sys.exit("Could not parse winws.exe command from batch file")

executable = start_match.group(1).strip()
sys.exit("Ошибка: winws.exe не найден в батнике")

executable = str(bin_dir / "winws.exe")
args = start_match.group(2).strip().replace('^', '').replace('\n', ' ').strip()
executable = executable.replace("%BIN%", str(bin_path)+"\\").replace("%LISTS%", str(lists_path)+"\\").replace("%CONFIGS%", str(configs_path)+"\\")
args = args.replace("%BIN%", str(bin_path)+"\\").replace("%LISTS%", str(lists_path)+"\\").replace("%CONFIGS%", str(configs_path)+"\\")
logging.info(f"EXECUTABLE: {executable}")
logging.info(f"ARGS: {args}")

if not Path(executable).exists():
logging.error(f"Исполняемый файл не найден: {executable}")
sys.exit(f"Executable not found: {executable}")

# 3. Заменяем макросы путей
replacements = {
"%BIN%": str(bin_dir) + "\\",
"%LISTS%": str(lists_dir) + "\\",
"%CONFIGS%": str(configs_dir) + "\\",
"%~dp0": str(base_zapret) + "\\"
}

for macro, real_path in replacements.items():
args = args.replace(macro, real_path)
executable = executable.replace(macro, real_path)

# Очистка путей от двойных слешей
args = args.replace("\\\\", "\\")

logging.info(f"Команда готова. EXE: {executable}")
return executable, args


def create_service(batch_path, display_version):
"""Create a Windows service from a batch file."""
"""Создание службы."""
executable, args = parse_bat_file(batch_path)
service_display = f"Moonstone Zapret DPI Bypass version[{display_version}]"
service_display = f"Sakura Flow DPI Bypass version[{display_version}]"
quoted_exe = f'"{executable}"' if ' ' in str(executable) else str(executable)
bin_path_value = f'{quoted_exe} {args}'

# Properly quote the executable path if it contains spaces
# For sc.exe binPath, the executable must be quoted if it has spaces
if ' ' in str(executable):
quoted_executable = f'"{executable}"'
else:
quoted_executable = str(executable)

# Construct the full binPath - the entire value needs to be quoted
# and the executable within it should also be quoted if it has spaces
bin_path_value = f'{quoted_executable} {args}'

# For sc.exe, use subprocess.run directly without shell to avoid quote interpretation issues
# sc.exe expects parameters like "binPath= value" where value can contain spaces
cmd_args = [
'sc.exe', 'create', SERVICE_NAME,
'start=', 'auto',
'displayname=', service_display,
'binPath=', bin_path_value
'sc.exe', 'create', SERVICE_NAME, 'start=', 'auto',
'displayname=', service_display, 'binPath=', bin_path_value
]

logging.info(f"Создание службы '{SERVICE_NAME}' с отображаемым именем '{service_display}'...")
logging.info(f"Выполнение команды: {' '.join(cmd_args)}")
try:
result = subprocess.run(cmd_args, capture_output=True, text=True, encoding=ENCODING)
if result.stdout:
logging.info(f"Вывод команды: {result.stdout.strip()}")
if result.stderr:
logging.error(f"Ошибка команды: {result.stderr.strip()}")
if result.returncode != 0:
logging.error(f"Не удалось создать службу: {result.stderr}")
sys.exit(f"Failed to create service: {result.stderr}")
except Exception as e:
logging.error(f"Исключение при выполнении команды: {e}")
sys.exit(f"Failed to create service: {e}")

if service_exists():
logging.info(f"Служба '{SERVICE_NAME}' успешно создана.")
else:
logging.error(f"Служба '{SERVICE_NAME}' не была создана.")
sys.exit(f"Service '{SERVICE_NAME}' was not created.")

subprocess.run(cmd_args, capture_output=True, text=True, encoding=ENCODING)

def start_service(batch_path, display_version):
"""Start the service, recreating it if necessary."""
"""Запуск службы."""
if service_exists():
logging.info("Служба существует, остановка и удаление для пересоздания с новым .bat...")
stop_service()
delete_service()
create_service(batch_path, display_version)
logging.info(f"Запуск службы '{SERVICE_NAME}'...")
result = run_cmd(f'sc.exe start "{SERVICE_NAME}"')
if result and result.returncode != 0:
logging.error(f"Не удалось запустить службу: {result.stderr}")

run_cmd(f'sc.exe start "{SERVICE_NAME}"')

def stop_service():
"""Stop the service."""
"""Остановка."""
if service_exists():
logging.info(f"Остановка службы '{SERVICE_NAME}'...")
run_cmd(f'sc.exe stop "{SERVICE_NAME}"')
# Дополнительно останавливаем Windivert если вдруг запущена
logging.info(f"Остановка службы 'WinDivert'...")
run_cmd(f'sc.exe stop "WinDivert"')

run_cmd('sc.exe stop "WinDivert"')

def delete_service():
"""Delete the service."""
"""Удаление."""
if service_exists():
logging.info(f"Удаление службы '{SERVICE_NAME}'...")
run_cmd(f'sc.exe delete "{SERVICE_NAME}"')

Loading