Система — это две независимые части: бэкенд и клиент. Они общаются только через REST API. Это не монолит — если упадёт сервер, лаунчер сохранит последний закэшированный конфиг и продолжит показывать серверы (без истории).
Node.js Backend
Express.js, CommonJS, cluster-режим. Три SQLite-базы, файловый конфиг серверов, JWT, Steam OAuth через Passport.js. Отвечает за авторизацию, биллинг, хранение истории, раздачу конфига.
Avalonia Launcher
.NET 10, Avalonia UI, паттерн MVVM. Только Windows. Получает онлайн серверов из REST-кэша бэкенда, рендерит UI нативно через GPU. С сервером общается за конфигом, онлайном, историей и аутентификацией.
SQLite × 3
Три отдельных файла БД для трёх задач: пользователи/аккаунты, история онлайна серверов, отзывы и баны. Разделены намеренно — разные нагрузки, разные паттерны записи.
┌─────────────────────┐ HTTPS / REST ┌──────────────────────────────┐ │ Avalonia Launcher │ ◄────────────────────► │ Node.js / Express │ │ (.NET 10, Win) │ (конфиг, онлайн, │ Cluster (VPS, port 443) │ └─────────────────────┘ история, auth) └──────────────────────────────┘ │ │ UDP A2S (централизованный опрос) ▼ ┌──────────────────────────────────────┐ │ DayZ серверы │ │ (любые IP:порт) │ └──────────────────────────────────────┘ │ read / write ▼ ┌──────────────────────────────────────┐ │ users.db / history.db / reviews.db │ │ + JSON-конфиг │ └──────────────────────────────────────┘
Node.js однопоточный, но на VPS несколько ядер. Бэкенд работает в cluster-режиме: master-процесс поднимает по одному воркеру на каждое CPU-ядро, каждый воркер — полноценный Express-сервер. При падении воркера master автоматически поднимает новый — zero downtime при любом краше.
Последовательность при первом запуске и при каждом последующем:
- Проверка обновления — лаунчер сравнивает свою версию с
launcher_versionиз конфига. Если есть новее — показывает уведомление, скачивает инсталлятор поupdateUrl. - Загрузка конфига — GET /api/config возвращает весь список серверов, premium-проекты, настройки. Кэшируется локально на случай офлайна.
- Получение онлайна — лаунчер запрашивает актуальный кэш онлайна с серверной инфраструктуры по REST. Данные уже готовы — кластер обновляет их в фоне каждые 10 минут, без нагрузки на клиента.
- Восстановление сессии — если в хранилище есть JWT-токен, лаунчер делает GET /api/me и проверяет его валидность. Если токен протух — просит залогиниться снова.
Это не случайность. У каждой базы свой паттерн нагрузки: пользовательская база читается при каждом логине, история — пишется пачками каждые 10 минут и редко читается, отзывы — случайные записи и частые чтения. Если держать всё в одной базе, WAL-журнал будет общим — пачки INSERT в историю будут блокировать чтение пользователей. Разделение устраняет это.
Пользовательская БД
Аккаунты владельцев серверов. Логин, bcrypt-хэш пароля, email, внутренний баланс в рублях, текущий план, ID проекта, Steam ID. Управляется через отдельный модуль с промисифицированными методами — никаких прямых SQL из основного кода.
История онлайна
Только одна таблица, только INSERT и SELECT по диапазону дат. Каждые 10 минут приходит пачка из N строк (по числу серверов). Индекс по (server_id, created_at) — выборка за 30/90 дней быстрая даже при миллионах записей.
Отзывы и баны
Две таблицы: отзывы и баны устройств/Steam-аккаунтов. Мягкое удаление — отзыв не удаляется физически, просто ставится флаг. Это позволяет восстановить ошибочно удалённый отзыв без бэкапа.
Конфиг администраторов
Хранится не в БД, а в зашифрованном JSON-файле: логины и хэши паролей администраторов, JWT-секрет. При первом запуске генерируется автоматически с рандомным криптостойким секретом через crypto.randomBytes.
PRAGMA journal_mode = WAL и synchronous = NORMAL. WAL (Write-Ahead Logging) позволяет параллельные чтения без блокировки записи — читатели не ждут, пока завершится пачечный INSERT в историю. На практике это разница между 5ms и 500ms при нагрузке.| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | Автоинкремент |
| server_id | TEXT | Идентификатор сервера — ip:port или ID из конфига. Индексирован. |
| players | INTEGER | Онлайн в момент снепшота (из A2S-ответа) |
| max_players | INTEGER | Максимальный слот в момент снепшота |
| created_at | DATETIME | UTC-время снепшота. DEFAULT CURRENT_TIMESTAMP. По этому полю строится график. |
| Колонка | Тип | Описание |
|---|---|---|
| id | INTEGER PK | Автоинкремент |
| server_id | TEXT | К какому серверу относится отзыв |
| device_id | TEXT | UUID устройства — генерируется лаунчером при первом запуске, хранится локально |
| steam_id | TEXT | Steam ID64 (если пользователь авторизован). NULL для анонимных. |
| is_anonymous | INTEGER | 1 = только device_id, 0 = от Steam-аккаунта |
| rating | INTEGER | 1–5 звёзд |
| text | TEXT | Текст отзыва после фильтрации |
| is_deleted | INTEGER | Мягкое удаление. 1 = скрыт, 0 = виден. Физически не удаляется. |
| created_at | DATETIME | UTC-время создания |
В системе два типа привилегированных пользователей. Ключевой момент: у них разные JWT-секреты и разные middleware для проверки. Это сделано намеренно — токен admin'а физически не пройдёт через middleware владельца и наоборот. Никакой логики "если есть role=admin, то пропустить везде" — нет.
Владельцы серверов
Аккаунты в пользовательской БД. Регистрируются сами через форму. Пароль хэшируется bcrypt с cost 12 (это ~250ms на современном CPU — намеренно медленно, чтобы брутфорс был нерентабелен). Токен живёт 30 дней.
Администраторы
Логины в конфигурационном файле, не в БД. Это значит что добавить/удалить администратора — это правка файла и рестарт, не SQL. Есть роли: admin и super_admin. Super_admin может делать то, что обычный admin — нет (например, удалять пользователей).
Steam авторизация сделана через Passport.js + Steam OpenID. Важно: она используется только чтобы привязать отзыв к реальному Steam-аккаунту. Войти в личный кабинет через Steam нельзя — это другая система.
После коллбэка сессия содержит req.user.id (Steam ID64) и req.user.displayName. При следующем POST /api/reviews этот Steam ID привязывается к отзыву. Steam ID публичен сам по себе — мы не получаем ничего, что пользователь не мог бы узнать самостоятельно.
Лаунчер хранит JWT в защищённом локальном хранилище. При каждом запуске проверяет токен через GET /api/me. Если сервер вернул 401 — токен инвалидирован, показывает экран логина. Refresh-токенов нет — при истечении 30 дней просто снова логин. Это сделано намеренно: меньше сложности, меньше векторов атаки.
is_banned в БД — middleware проверяет его при каждом запросе.Весь A2S-опрос вынесен на централизованный Node.js кластер — и это не случайность. Во-первых, защита от DDoS: если бы каждый клиент сам слал UDP-пакеты, любой сервер мог бы оказаться под распределённым флудом от тысяч лаунчеров одновременно. Во-вторых, обход клиентских файрволов: многие провайдеры режут исходящий UDP-спам — пользователь просто не получил бы онлайн. Лаунчер избавлен от этих проблем: он запрашивает готовый кэш по HTTPS REST и получает данные всегда, независимо от настроек сети.
Steam Web API для этих задач не подходит — rate limit в несколько тысяч запросов в сутки при 50+ серверах с обновлением каждые минуты физически невозможен. Плюс задержка данных 1–5 минут делает его бесполезным для отображения живого онлайна.
A2S (Source Engine Query Protocol) — UDP-протокол, который поддерживает любой Source-движок, включая DayZ. Кластер отправляет запрос на query-порт сервера (игровой порт + 1, это стандарт Source Engine), получает ответ за <50ms и парсит бинарный ответ по спецификации протокола. Сервер может ответить challenge — это стандартная анти-спуф защита протокола, кластер обрабатывает её автоматически.
Если за 5 секунд ответа нет — сервер считается оффлайн. Весь цикл по всем серверам идёт параллельно через Promise.allSettled, что означает: падение одного сервера не задерживает опрос остальных.
Используется гибридная модель. Список активных модов сервера приходит с бэкенда в составе конфига — кластер извлекает его из A2S-ответа (DayZ кладёт Workshop ID модов в поле keywords/gametype, это официальное поведение игры). Локальная валидация кэша и путей идёт строго через нативный steam_api64.dll по интерфейсу SteamUGC — именно он отвечает за проверку установленных Workshop-предметов без лишних запросов к Steam Web API.
Вся конфигурация серверов живёт в одном JSON-файле на сервере. Лаунчер получает его при каждом запуске через GET /api/config. Этот файл содержит: список всех серверов (обычных и premium), список Super Premium проектов с хабами, версию лаунчера, URL обновления и ссылки.
{
"global_settings": {
"launcher_version": "2.4.0", // лаунчер сравнивает со своей, если старше — обновление
"tracked_mods": [] // глобальный список отслеживаемых Workshop-модов
},
"premium_projects": [ // Super Premium: полноценные хабы
{
"project_id": "my_project",
"name": "Название проекта",
"owner_login": "owner123", // привязка к аккаунту — для проверки прав
"hub_url": "https://...", // URL для встроенного браузера в хабе
"servers": [
{ "ip": "1.2.3.4", "port": 2302, "name": "Main" }
],
"discord_webhook": "https://discord.com/api/webhooks/..."
}
],
"servers": { // Basic Premium — по одному серверу
"1.2.3.4:2302": {
"name": "Название сервера",
"plan": "basic",
"verified": true,
"icon_url": "/uploads/icons/xxx.png",
"banner_url": "/uploads/banners/xxx.png"
}
},
"updateUrl": "https://...", // ссылка на инсталлятор новой версии
"discordUrl": "https://..."
}
Когда владелец редактирует настройки в личном кабинете, middleware проверяет что owner_login в его части конфига совпадает с логином из JWT. Он не может трогать чужие секции — только свой servers[ip:port] или свой premium_projects объект.
configCache. Остальные воркеры перечитают файл при следующем запросе к /api/config. Окно несогласованности — время между запросами, обычно секунды.Плеер — это не веб-вью и не Electron-обёртка над YouTube. Это нативный компонент внутри Avalonia-приложения. Он использует внешний аудио API для поиска и стриминга треков, получая прямые аудио-потоки которые воспроизводит нативным декодером.
Поиск треков
Запрос к внешнему API по названию/исполнителю. API возвращает список треков с метаданными и URL потока. 70+ млн треков — это размер каталога, к которому есть доступ. Сам каталог не хранится локально.
Импорт из платформ
Пользователь подписывает свои треки с внешних платформ прямо в лаунчер — это синхронизация плейлиста, не загрузка файлов. Треки стримятся по запросу, не хранятся локально.
Визуализация плазмы
Во время воспроизведения декодер вычисляет FFT (быстрое преобразование Фурье) аудиосигнала в реальном времени. Амплитуды частотных полос передаются в рендерер плазменного фона — он меняет скорость и интенсивность анимации синхронно с ритмом.
Алгоритм «Моя Волна»
Умная подборка треков на основе текущего трека (BPM, жанр, тональность), времени суток и истории прослушивания. Реализован через API рекомендаций — не машинное обучение на клиенте. Каждая сессия звучит иначе, но всегда в тему.
| Метод | Путь | Доступ | Что делает |
|---|---|---|---|
| GET | /api/config | Public | Полный конфиг — список серверов, premium-проекты, версия лаунчера, ссылки. Главный эндпоинт. |
| POST | /api/login | Public | Аутентификация владельца. Возвращает JWT-токен на 30 дней. |
| POST | /api/register | Public | Регистрация нового владельца. bcrypt хэширование, запись в БД. |
| GET | /api/me | Owner JWT | Данные текущего аккаунта: баланс, план, ID проекта, дата окончания подписки. |
| POST | /api/owner/server | Owner JWT | Обновить настройки своего сервера в конфиге: имя, иконка, баннер, Discord webhook. |
| GET | /api/history/:id | Owner JWT | История онлайна за N дней. Basic — 30 дней, Super — 90. Проверяет что сервер принадлежит этому аккаунту. |
| POST | /api/balance/topup | Owner JWT | Создать счёт на пополнение баланса через платёжный провайдер. Возвращает URL для оплаты. |
| POST | /api/payment/callback | HMAC подпись | Webhook от платёжного провайдера. Верифицирует подпись, зачисляет баланс. Идемпотентный — повторный вызов с тем же ID не задвоит деньги. |
| POST | /api/subscribe | Owner JWT | Активировать/продлить подписку. Списывает с баланса, обновляет план и срок в конфиге. |
| GET | /api/reviews/:id | Public | Список отзывов для сервера. Пагинация, средняя оценка, фильтр is_deleted=0. |
| POST | /api/reviews | Device ID | Оставить отзыв. Один device_id или Steam ID — один отзыв на сервер. Дубли отклоняются. |
| GET | /api/admin/users | Admin JWT | Список всех owner-аккаунтов с балансами, планами, статусами. |
| POST | /api/admin/balance | Admin JWT | Ручное зачисление баланса (для крипто-оплат, подтверждённых вручную). |
| DELETE | /api/admin/review/:id | Admin JWT | Мягкое удаление отзыва (is_deleted=1). Опционально — бан device_id или Steam ID. |
requireOwnerAuth и requireAdminAuth — физически разные функции с разной логикой проверки. Admin-токен не пройдёт через requireOwnerAuth и наоборот. Это намеренно — нет никакого "суперпользователя" который проходит везде.Каждый воркер держит несколько объектов в памяти — чтобы не лезть в файл или базу на каждый запрос:
let configCache = null; // конфиг серверов — читается с диска при первом запросе let adminsConfig = null; // конфиг администраторов let globalServerListCache = []; // последний A2S-результат по всем серверам let modUpdateCache = {}; // { 'ip:port': { mods: [], ts: Date } } let premiumModsCache = {}; // то же для premium-серверов
globalServerListCache — это последний известный статус всех серверов (онлайн, слоты). Лаунчер получает его вместе с конфигом в одном ответе. Обновляется фоновым циклом — лаунчер никогда не ждёт свежего A2S от сервера, он всегда получает последний закэшированный результат.
- Берёт полный список серверов из
configCache - Параллельно опрашивает все серверы по UDP через
Promise.allSettled - Обновляет
globalServerListCacheактуальными данными - Пишет снепшот в историю пачечным INSERT — одна транзакция на все серверы
- Если Super Premium сервер был онлайн → стал оффлайн → стреляет в Discord webhook
Promise.allSettled вместо Promise.all — ключевое решение. Если один сервер не отвечает и таймаут 5 секунд — остальные не ждут, цикл продолжается. Весь мониторинг 50 серверов занимает ~5 секунд (время таймаута одного упавшего), а не 50×5 секунд.История не чистится автоматически — это намеренно. Старые данные (старше 90 дней) просто не показываются на графике, но хранятся. Можно добавить VACUUM по расписанию если база вырастет до неприемлемого размера.
Прямых рекуррентных списаний с карты нет. Владелец пополняет внутренний баланс (число в рублях в пользовательской БД), затем сам активирует или продлевает подписку — средства списываются с баланса. Плюсы этой модели:
- Не нужна согласованность платёжных систем на рекуррентные списания
- Возврат = просто увеличить баланс, не нужна операция возврата у провайдера
- Пользователь сам контролирует когда платит — нет неожиданных списаний
- Одна модель работает для всех способов оплаты (карта, крипта, ручное зачисление)
const PLAN_PRICES = { basic: 2000, // ₽/мес super: 5000, // ₽/мес }; const EXTRA_SERVER_PRICE = 500; // ₽/мес за каждый сервер сверх первого function calcPrice(plan, days, serverCount = 1) { const base = PLAN_PRICES[plan] * days / 30; const extra = Math.max(0, serverCount - 1) * EXTRA_SERVER_PRICE * days / 30; return Math.round(base + extra); }
Платёжный провайдер присылает webhook при успешном платеже. Сервер:
- Проверяет HMAC-подпись запроса — без этого любой мог бы сфейкать пополнение
- Проверяет что платёж с таким ID ещё не обработан (идемпотентность)
- Добавляет сумму к балансу владельца в БД
- Логирует транзакцию с временной меткой и ID платежа
Карта / СБП
Через платёжный агрегатор. Автоматический webhook → верификация подписи → зачисление баланса. Пользователь видит пополнение сразу после подтверждения платежа.
USDT TRC20
Ручной процесс. Пользователь переводит на указанный кошелёк, пишет в поддержку с хэшем транзакции. Администратор проверяет в блокчейне и зачисляет через POST /api/admin/balance.
Когда пользователь скачивает и запускает .exe-файл, Windows SmartScreen проверяет его. Если файл не подписан или подписан дешёвым сертификатом — показывается красное окно "Неизвестный издатель". Кнопки "Запустить" нет — только "Больше сведений" и "Всё равно запустить". Большинство пользователей закроют и не вернутся.
Code Signing сертификат — это криптографическая подпись, встроенная в .exe. Windows проверяет её и видит: файл не изменялся с момента подписания, и мы знаем кем он подписан.
DV (Domain Validation)
Центр сертификации проверяет только владение доменом. Для HTTPS-сайтов — нормально. Для подписи кода — не подходит: имя организации не проверяется, SmartScreen продолжает показывать предупреждение.
OV (Organization Validation)
CA проверяет реальные данные организации или физлица. В подписи отображается название. SmartScreen убирает красное окно после набора репутации (несколько тысяч чистых установок).
EV (Extended Validation)
Максимальная проверка, физический HSM-токен. SmartScreen доверяет сразу без набора репутации. Дороже и сложнее в получении — нужен при выпуске первой версии.
Каждый лаунчер при первом запуске генерирует UUID (device_id) и хранит его локально. Этот ID отправляется с каждым отзывом. Сервер проверяет: если device_id уже оставил отзыв на этот сервер — дубль отклоняется. Одно устройство = один голос.
Если пользователь авторизован через Steam — дополнительно проверяется Steam ID64. Смена device_id не помогает если Steam ID уже голосовал. Оба механизма работают параллельно.
На эндпоинтах логина и регистрации стоит rate limiter по IP. Это стандартная защита от брутфорса — после N попыток за окно времени IP получает 429. bcrypt с cost 12 добавляет ещё один барьер: даже при пропуске rate limit'а каждая проверка занимает ~250ms.
На публичных эндпоинтах (конфиг, отзывы) rate limiting мягче — защита от парсеров, не от пользователей.
Загрузка через multer с ограничениями: только изображения по MIME-типу, ограничение размера. Файлы сохраняются в директорию /uploads/ с рандомным именем — имя оригинального файла игнорируется, чтобы не было path traversal. Старый файл удаляется при загрузке нового.
- Не модифицирует и не читает файлы DayZ или Steam
- Не вмешивается в анти-чит (BattlEye и VAC работают независимо)
- Не хранит пароль Steam — авторизация через официальный OpenID, пароль до нас не доходит
- Не собирает данные об установленных программах или системе
- Не делает запросы в фоне когда лаунчер закрыт
Все настройки оптимизации работают исключительно через официальные Windows API — никакой модификации игровых файлов, никакого вмешательства в анти-чит. По сути это то, что опытный геймер делал бы вручную через диспетчер задач и командную строку, но автоматически и в один клик.
S1 — это комплексный режим, который применяет несколько системных оптимизаций одновременно в момент запуска игры:
Таймер разрешения
Вызов timeBeginPeriod(1) через Win32 API снижает разрешение системного таймера с дефолтных 15.6ms до 1ms. Это уменьшает джиттер в сетевых пакетах и делает фреймтайм стабильнее. Применяется на время сессии, сбрасывается при выходе.
CPU affinity и планировщик
Процесс DayZ.exe привязывается к определённым ядрам через SetProcessAffinityMask, оставляя часть ядер под систему. Это снижает конкуренцию за кэш L3 между игрой и фоновыми процессами.
Сетевые параметры
Через реестр и netsh временно применяются настройки TCP: отключается Nagle-алгоритм (TcpNoDelay) для уменьшения задержки, корректируется размер буфера приёма. Всё откатывается при закрытии игры.
Параметры запуска
К командной строке DayZ добавляются launch-параметры: -noPause, -noSplash, -malloc=system и другие — в зависимости от конфига. Это стандартные параметры самой игры, не патч.
Очистка RAM
Перед запуском игры лаунчер вызывает EmptyWorkingSet для всех некритичных процессов — браузеров, мессенджеров и прочего. Это не убивает процессы, а переводит их страницы памяти в page file, освобождая физическую RAM для DayZ. DayZ жрёт много — каждый свободный гигабайт на счету.
Высокий приоритет процесса
После запуска DayZ.exe лаунчер меняет приоритет процесса через SetPriorityClass(handle, HIGH_PRIORITY_CLASS). Планировщик Windows отдаёт CPU-время высокоприоритетным процессам в первую очередь. Лаунчер при этом сам переключается на ниже нормального — чтобы не конкурировать с игрой.
Ограничение ресурсов лаунчера
В режиме "игра запущена" лаунчер сам себя ограничивает: снижает приоритет своих потоков, уменьшает частоту обновления UI до минимума, отключает фоновые задачи (опрос серверов, кэш обновлений). Цель — отдать максимум ресурсов DayZ.
Мониторинг в фоне
Пока игра запущена, лаунчер отслеживает PID и состояние процесса. Когда DayZ закрывается — автоматически откатывает все изменения приоритетов, таймеров и сетевых параметров, и возобновляет свою нормальную работу.
VPN-детектор
При запуске лаунчер перебирает сетевые адаптеры через GetAdaptersInfo / WMI и ищет признаки VPN-адаптера: тип интерфейса IF_TYPE_PPP или IF_TYPE_TUNNEL, характерные имена драйверов (TAP, WireGuard, OpenVPN и аналоги). Если VPN обнаружен — показывает предупреждение, потому что многие серверы банят VPN-адреса.
Переключатель BattlEye
BattlEye управляется параметром запуска -bepath=... и наличием BE-директории. Когда BattlEye отключён — лаунчер передаёт -noBattlEye в командной строке DayZ. Это нужно для модифицированных серверов, где BE выключен владельцем. На серверах с включённым BE это не поможет — сервер сам проверяет клиента.
Плазма — это процедурно-анимированный фон на базе шейдера. Несколько слоёв синус-волн с разными частотами, фазами и скоростями складываются в органичное плавное движение. Цветовая схема и скорость — настраиваемые параметры.
Аудио поток → FFT (быстрое преобразование Фурье) → массив амплитуд по частотам ↓ Bass (20–200 Hz) → скорость и масштаб волн плазмы Mid (200–2000 Hz) → интенсивность цвета и яркость High (2000+ Hz) → мелкие детали и частота пульсации
FFT вычисляется в отдельном потоке с частотой ~60 раз в секунду. Результат передаётся в рендерер как массив float-значений. Шейдер плазмы читает эти значения и модифицирует параметры волн в реальном времени — фон буквально "дышит" в такт музыке.
Когда музыка не играет — плазма работает в "idle" режиме с медленной спокойной анимацией. Когда пауза — плавно переходит в idle через интерполяцию, без резкого скачка.
Масштаб интерфейса
Avalonia имеет встроенную поддержку DPI-независимого масштабирования. Слайдер масштаба в настройках меняет глобальный LayoutTransform корневого контейнера. Изменение применяется мгновенно без перезапуска — всё перерисовывается в рамках следующего layout-прохода Avalonia.
Прозрачность панелей
Каждая панель лаунчера использует полупрозрачный фон через Opacity в XAML-стилях. Слайдер пишет значение в глобальный ресурс (ResourceDictionary), от которого зависят все панели. Все панели реагируют одновременно без перебора элементов.
Blur-эффект
Реализован через BlurEffect в Avalonia или нативный Acrylic (если поддерживается системой и включён в настройках Windows). Применяется к подложке панелей — размывает то что за ними. На слабых GPU можно отключить без потери функциональности.
Режим для дальтоников
Применяет матрицу цветовой коррекции ко всему рендереру. Три пресета: протанопия (красный), дейтеранопия (зелёный), тританопия (синий). Матрица подобрана так чтобы ключевые UI-элементы (статус серверов, уведомления) оставались различимы при любом типе нарушения.
Discord Activity (Rich Presence)
Реализован через Discord IPC — именованный пайп \\.\pipe\discord-ipc-0 (и до -9 если занят). Лаунчер подключается к пайпу, авторизуется с Application ID, затем отправляет SET_ACTIVITY команды. Обновление — каждые ~15 секунд или при смене состояния (в меню / смотрит сервер / в игре). Discord сам контролирует частоту отображения.
Свободное перетаскивание окна
Лаунчер — окно без системного titlebar (кастомный). По умолчанию такое окно нельзя перетащить кроме как за узкую область. Решение: переопределить HitTest на фоне и пустых областях UI — они возвращают HitTestResult.Caption, что говорит Windows "это titlebar, можно тащить". Кнопки и интерактивные элементы возвращают Client — их перетаскивание не триггерит move.
Смена языка без перезапуска
Локализация через Avalonia ResourceDictionary с XAML-ресурсами для каждого языка. При смене языка в настройках лаунчер меняет активный MergedDictionary в корневых ресурсах приложения. Avalonia автоматически обновляет все Binding'и, которые ссылаются на локализованные ресурсы — перезапуск не нужен.
Кастомные акцентные цвета
Базовая цветовая схема лаунчера задаётся через глобальные ресурсы Avalonia (аналог CSS-переменных). Смена темы или акцентного цвета — это замена значений в ResourceDictionary во время выполнения. Все элементы UI, которые ссылаются на эти ресурсы, перерисовываются автоматически.