Система — это две независимые части: бэкенд и клиент. Они общаются только через REST API. Это не монолит — если упадёт API, лаунчер сохранит последний закэшированный конфиг и продолжит показывать серверы (без истории и без онлайна реального времени).
Node.js Backend
Express.js, cluster-режим, несколько изолированных сервисов под общим nginx. Локальные SQLite-базы, файловый конфиг серверов, JWT, Steam OAuth через Passport.js. Отвечает за авторизацию, биллинг, хранение истории, раздачу конфига и музыкальный модуль.
Avalonia Launcher
.NET 10, Avalonia UI, паттерн MVVM. Только Windows. Нативный GPU-рендер (Skia). Получает онлайн серверов из REST-кэша бэкенда, общается с API за конфигом, онлайном, историей и аутентификацией.
Edge: CDN + WAAP
Перед бэкендом стоит CDN с WAAP-фильтром: терминация TLS, DDoS-защита, Bot Management, кэш статики. Origin доступен только из доверенных подсетей CDN — прямой стук в обход не проходит.
┌─────────────────────┐ HTTPS / REST ┌──────────────────────────────┐ │ Avalonia Launcher │ ◄────────────────────► │ CDN + WAAP (edge) │ │ (.NET 10, Win) │ (TLS termination, │ DDoS / Bot Management │ └─────────────────────┘ кэш статики) └──────────────────────────────┘ │ │ origin-pull HTTP, whitelist IP ▼ ┌──────────────────────────────────────┐ │ nginx → Node.js cluster │ │ signal-api + signal-music сервисы │ └──────────────────────────────────────┘ │ │ UDP A2S (централизованный опрос) ▼ ┌──────────────────────────────────────┐ │ DayZ серверы │ │ (любые IP:порт) │ └──────────────────────────────────────┘ │ read / write ▼ ┌──────────────────────────────────────┐ │ локальные SQLite (WAL) + JSON-конфиг │ │ аккаунты / история / отзывы / музыка│ └──────────────────────────────────────┘
Node.js однопоточный, но на VPS несколько ядер. Бэкенд работает в cluster-режиме: master поднимает воркеров, каждый — полноценный Express-сервер. Под общим nginx живут два сервиса: основной API (signal-api) и музыкальный модуль (signal-music) — у каждого свои воркеры, чтобы тяжёлые операции одного не блокировали другой. При падении воркера 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 — намеренно медленно, чтобы брутфорс был нерентабелен). Токен живёт 7 дней.
Администраторы
Логины в конфигурационном файле, не в БД. Это значит что добавить/удалить администратора — это правка файла и рестарт, не 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-токенов нет — по истечении срока — повторный логин. Это сделано намеренно: меньше сложности, меньше векторов атаки.
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. Окно несогласованности — время между запросами, обычно секунды.| Метод | Путь | Доступ | Что делает |
|---|---|---|---|
| GET | /api/servers/global | Public | Список серверов: глобальный кэш + premium-проекты. Главный эндпоинт лаунчера. |
| POST | /api/auth/login | Public | Аутентификация. Возвращает JWT-токен на 7 дней + SSO-cookie для admin/owner-страниц. |
| POST | /api/auth/register | Public | Регистрация пользователя. bcrypt cost 12, email-верификация шестизначным кодом. |
| GET | /api/auth/me | JWT | Текущий аккаунт: роль, баланс, план, ID проекта, срок подписки. |
| POST | /api/auth/verify-email | Public | Подтверждение email кодом из SMTP-письма. После успеха — финализация регистрации. |
| GET | /api/owner/project | Owner JWT | Получить настройки своего проекта: имя, иконка, баннер, серверы, Discord webhook. |
| POST | /api/owner/project | Owner JWT | Обновить настройки своего проекта. Проверка ownership через JWT payload. |
| GET | /api/server/stats | Public | Подробная статистика сервера: онлайн, графики 24ч/7д/30д, отзывы, средний онлайн. |
| POST | /api/user/payment/create | JWT | Создать платёж через платёжный провайдер. Возвращает URL/реквизиты для оплаты. |
| POST | /api/user/subscription/renew | Owner JWT | Продлить подписку. Списывает с внутреннего баланса, продлевает срок. |
| GET | /api/reviews/:serverId | Public | Отзывы сервера. Пагинация, средняя оценка, фильтр is_deleted=0. |
| POST | /api/reviews/:serverId | Device ID | Оставить отзыв. Один device_id или Steam ID — один отзыв на сервер. |
| GET | /api/reviews/bulk | Public | Массовая агрегация лайков/дизлайков по списку server_id для сортировки списка. |
| GET | /api/admin/subscriptions | Admin JWT | Список всех owner-аккаунтов с балансами, планами, статусами. |
| POST | /api/admin/balance/adjust | Admin JWT | Ручное зачисление/списание баланса администратором (корректировки, компенсации, спорные платежи). |
| GET | /api/admin/launcher-stats | Admin JWT | Активные пользователи лаунчера за последние 5 мин + счётчик скачиваний установщика. |
| GET | /api/admin/server-load | Admin JWT | Нагрузка VPS: CPU по потокам, RAM, канал интернета. Замер по требованию. |
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 по расписанию если база вырастет до неприемлемого размера.
Прямых рекуррентных списаний с карты нет. Владелец пополняет внутренний баланс (число в рублях в пользовательской БД), затем сам активирует или продлевает подписку — средства списываются с баланса. Плюсы этой модели:
- Не нужна согласованность платёжных систем на рекуррентные списания
- Возврат = просто увеличить баланс, не нужна операция возврата у провайдера
- Пользователь сам контролирует когда платит — нет неожиданных списаний
- Одна модель работает для всех способов оплаты (Robokassa, Альфа-эквайринг, ручная корректировка)
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 платежа
Robokassa
Платёжный агрегатор: банковские карты, СБП, ЮMoney. Автоматический webhook → верификация подписи → зачисление баланса. Пользователь видит пополнение сразу после подтверждения платежа.
Альфа-Банк
Интернет-эквайринг АО «Альфа-Банк»: приём карт Visa / MasterCard / МИР напрямую от банка. Callback с верификацией подписи → автоматическое зачисление баланса.
Когда пользователь скачивает и запускает .exe-файл, Windows SmartScreen проверяет его. Если файл не подписан или подписан дешёвым сертификатом — показывается красное окно "Неизвестный издатель". Кнопки "Запустить" нет — только "Больше сведений" и "Всё равно запустить". Большинство пользователей закроют и не вернутся.
Code Signing сертификат — это криптографическая подпись, встроенная в .exe. Windows проверяет её и видит: файл не изменялся с момента подписания, и мы знаем кем он подписан.
DV (Domain Validation)
Центр сертификации проверяет только владение доменом. Для HTTPS-сайтов — нормально. Для подписи кода — не подходит: имя организации не проверяется, SmartScreen продолжает показывать предупреждение.
OV (Organization Validation)
CA проверяет реальные данные организации или физлица. В подписи отображается название. SmartScreen убирает красное окно после набора репутации (несколько тысяч чистых установок).
EV (Extended Validation)
Максимальная проверка, физический HSM-токен. SmartScreen доверяет сразу без набора репутации. Дороже и сложнее в получении — нужен при выпуске первой версии.
Лаунчер собирается под Windows как single-file .exe с обфускацией коммерческим инструментом класса code-protector. Это означает:
- Имена классов, методов и полей в скомпилированной сборке переименованы в случайные последовательности — декомпилятор показывает кашу, не читабельную бизнес-логику.
- Строковые константы (URL, идентификаторы, маркеры) зашифрованы и расшифровываются только в рантайме перед использованием.
- Поток управления (control flow) переплетён — статический анализ путей выполнения сильно затруднён.
- Антиотладка и проверки целостности — попытка подцепиться отладчиком или подменить байт-код приводит к мгновенному выходу процесса.
Цель защиты — не сделать reverse engineering невозможным (это недостижимо), а сделать его экономически нецелесообразным. Раскрутить хорошо обфусцированный .NET-бинарь до уровня «понять архитектуру и достать ключи» — это часы-дни ручной работы специалиста, а не пара минут в dnSpy.
rotate-secrets + ручной триггер. История ключей не хранится — старые значения становятся невалидными мгновенно после ротации.Каждый лаунчер при первом запуске генерирует 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 это не поможет — сервер сам проверяет клиента.
Плазма — это процедурно-анимированный фон на базе SkSL-шейдера. Несколько слоёв синус-волн с разными частотами, фазами и скоростями складываются в органичное плавное движение. Цветовая схема и скорость — настраиваемые параметры в разделе Style.
Рендерится напрямую на GPU через Avalonia Skia backend, цикл анимации синхронизирован с VBlank монитора. Никакой нагрузки на CPU — даже на ноутбуке с интегрированной графикой это менее 1% утилизации GPU.
В режиме энергосбережения (опция в настройках) плазма отключается полностью и фон становится статичным — для слабых машин или ноутбука на батарее.
Масштаб интерфейса
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, которые ссылаются на эти ресурсы, перерисовываются автоматически.