# Other

# ТЗ Colombino Chat

# ColombinoChat — ТЗ

> Плагин управления чатами/PM/упоминаниями для сети Colombino (Paper + Velocity). Цель — единая система чатов с MiniMessage, PlaceholderAPI, антиспамом, логированием и сетевым хранением данных.

---

## 0) Техническая информация

- **Название плагина:** `ColombinoChat`
- **Поддерживаемые версии Paper:** `1.20.4+`
- **Платформа:** `Paper + Velocity`
- **Сборка:** `Uber-Jar` (общий артефакт) **или** один репозиторий с двумя модулями:
    
    
    - `colombinochat-paper`
    - `colombinochat-velocity`

---

## 1) Гайдлайны

### 1.1 YAML

- Формат путей: `example-path-to-something`
- Все пользовательские строки (уведомления/ошибки/подсказки) — в `messages.yml` и поддерживают MiniMessage.

### 1.2 Git / коммиты

- Conventional Commits по Angular (без scope): <span>https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit-message-header</span>
- Пример: `feat: add mentions cooldown`

### 1.3 Стек библиотек

- **CommandAPI не использовать.**
- Выбор библиотек (конфиг/БД/миграции) согласовать до старта, но без избыточных зависимостей.

---

## 2) Термины

- **Scope (уровень чата):** область видимости сообщения.
- **Network Global:** межсерверный чат по сети Velocity.
- **Server Global:** чат внутри одного Paper-сервера.
- **World:** чат внутри одного мира (World) на Paper.
- **Plot:** чат внутри плота PlotSquared (использовать встроенный Plot chat PlotSquared или интеграцию).

---

## 3) Архитектура и зоны ответственности

### 3.1 Velocity (прокси)

Отвечает за:

- **Network Global** (межсерверный чат)
- **Cross-server PM** (`/pm`, `/reply`)
- Синхронизированный **PM ignore** (персистентно)
- Хранение/синхронизацию данных через **сетевую БД PostgreSQL**
- Логи: Network Global, PM, blocked

### 3.2 Paper (бекенд)

Отвечает за:

- **Server/World/Plot** отображение (рендер в чат)
- Форматирование (Adventure/MiniMessage) и **PAPI**
- Валидацию/антиспам
- Mentions + звук + кулдауны
- Логи Server/World/Plot и blocked

---

## 4) Хранение данных (обязательное)

### 4.1 Общее правило

Все данные, требующие хранения и/или синхронизации в сети, должны храниться в **PostgreSQL**.

### 4.2 Что хранить в БД (минимум)

- UUID игрока → active-scope / active-channel (если включено сохранение)
- PM ignore-листы (UUID → UUID)
- Оффлайн-упоминания (notifications) с TTL и лимитами

### 4.3 Идентификаторы

- Везде использовать **UUID** (ник — только для отображения).

### 4.4 БД-слой

- Пул соединений (например HikariCP) — допускается/рекомендуется.
- Миграции (Flyway/Liquibase или собственный механизм) — согласовать до начала реализации.

---

## 5) Уровни чата (Scopes)

### 5.1 Обязательные scopes

1. **Network Global** — межсерверный (Velocity)
2. **Server Global** — внутри Paper
3. **World** — внутри текущего мира
4. **Plot** — внутри текущего плота (PlotSquared)

### 5.2 Поведение Plot scope

Если игрок не находится на плоту:

- **A)** блокировать отправку + уведомление отправителю  
    или
- **B)** fallback на `World` или `Server` (настраивается)

---

## 6) Переключение чатов и триггеры

### 6.1 Активный чат (state)

- У игрока есть `active-scope` (или `active-channel-id`).
- Настройки:
    
    
    - дефолтный scope при входе
    - сохранять ли active-scope в БД (при релоге)
    - синхронизировать ли active-scope в сети (при смене сервера в Velocity)

### 6.2 Команды переключения

Минимальный набор:

- `/chat network`
- `/chat server`
- `/chat world`
- `/chat plot`

Алиасы настраиваемые (пример): `/gc`, `/sc`, `/wc`, `/pc`.

Команды:

- переключают активный scope
- возвращают подтверждение (MM)

### 6.3 Триггеры в сообщениях (send-only / switch-and-send)

Примеры:

- `@network <сообщение>`
- `@server <сообщение>`
- `@world <сообщение>`
- `@plot <сообщение>`

Требования:

- возможность задавать любые алиасы тегов/префиксов для каждого scope
- режимы:
    
    
    - `send-only` (без смены active-scope)
    - `switch-and-send` (сменить active-scope и отправить)

> Допускается, что изменение алиасов команд/тегов может не поддерживать hot-reload.

### 6.4 Приоритет обработки

1. Команда (если используется команда отправки)
2. Триггер/префикс в сообщении
3. Активный scope игрока

---

## 7) Форматирование сообщений

### 7.1 MiniMessage в конфигурации

Во всех текстовых полях с форматированием / hover / click — MiniMessage.

### 7.2 Плейсхолдеры (встроенные)

- `&lt;username&gt;` — ник отправителя
- `&lt;message&gt;` — сообщение
- `&lt;timestamp&gt;` — Unix timestamp (host time)

Дополнительно:

- (Network) `<source_server_id>`
- (Network) `<source_server_name>` (маппинг ID→DisplayName на стороне Paper)
- (World) `&lt;world&gt;`
- (Plot) `<plot_id>` (если доступно)

### 7.3 PlaceholderAPI

- Поддержка PAPI плейсхолдеров в местах, где это имеет смысл
- Парсинг PAPI относительно **игрока-отправителя**

---

## 8) MiniMessage в сообщениях игроков и ссылки

### 8.1 MM в сообщении игрока

- MM парсится **только** при permission:
    
    
    - `colombinochat.message.minimessage`
- Без permission — plain text (без MM парсинга)

### 8.2 Автокликабельные ссылки (linkify)

- Навешивать ClickEvent средствами Adventure.
- Парсинг ссылок **только** при permission:
    
    
    - `colombinochat.message.links`

Требования:

- при отсутствии `http(s)://` добавлять `https://` (настраиваемо)
- не ограничивать современные TLD (не использовать 2–4 символа)
- опционально: whitelist/blacklist доменов

---

## 9) Система упоминаний (Mentions)

### 9.1 Упоминания

- Тег по никнейму (например `@Nickname`), паттерн настраиваем
- Подсветка упоминания задаётся отдельной настройкой (MiniMessage)

### 9.2 Звук и кулдаун

- При упоминании игроку воспроизводится звук (настраиваемо)
- Кулдаун на **получателя** (ms), чтобы исключить спам звуком

### 9.3 Оффлайн-упоминания (Notifications)

Если игрок упомянут, но оффлайн — создать уведомление:

- доставить при следующем входе
- при входе показать "Пока тебя не было: N упоминаний" и/или подсказку команды

Команды:

- `/mentions` — список уведомлений
- `/mentions clear` — очистить

Требования:

- Персистентно хранить в **PostgreSQL**
- TTL (например 7 дней) и лимит на игрока (например 50)

### 9.4 Автокомплит

- Комплит никнеймов онлайн игроков сети (Velocity) — по возможности

---

## 10) Cross-server PM (Личные сообщения)

Команды:

- `/pm <игрок> <сообщение>` — отправить PM
- `/reply <сообщение>` (`/r`) — ответ последнему собеседнику
- `/pmignore <игрок>` — добавить в игнор
- `/pmignore remove <игрок>` — убрать из игнора
- `/pmignore list` — список игнорируемых

Требования:

- Автокомплит никнеймов онлайн игроков сети (Velocity)
- Ignore хранится **персистентно** в PostgreSQL
- Отдельные форматы для входящих/исходящих PM (MiniMessage)

Опционально:

- звуки на отправку/получение PM (настраиваемо на Paper)
- SocialSpy:
    
    
    - `/socialspy` (toggle)
    - permission `colombinochat.socialspy`

---

## 11) Валидация сообщений (Regex)

Конфиг (пример):

```yaml
validation:
  enabled: true
  regex: "^[!\"@№;%:?*()_=#$^&\\/\\-—+`~;'<>\u005B\u005D{}|,.a-zA-Zа-яА-ЯёЁҐґЄєЇї0-9\\s]+$"

```

Требования:

- Валидация применяется к:
    
    
    - server/world/plot
    - network (до отправки на прокси)
    - pm
- При блокировке — уведомление отправителю (MM)

Permission bypass:

- `colombinochat.validation.bypass`

---

## 12) Фильтрация однотипных сообщений (Levenshtein Similarity)

Цель: блокировать спам “почти одинаковыми” сообщениями.

Настройки:

- `similarity.enabled`
- `similarity.threshold-percent` (0..100)
- `similarity.window-seconds` (например 60)
- `similarity.max-message-length` (лимит на расчёт)

Определение:

- сравнивать текущее сообщение с **последним** сообщением игрока в пределах `window-seconds`
- `similarity = 1 - (levenshtein / max(len1, len2))`
- блокировать, если `similarity*100 >= threshold-percent`

Нормализация (настраиваемо):

- lower-case
- trim
- collapse spaces
- optional: remove punctuation

Permission bypass:

- `colombinochat.similarity.bypass`

---

## 13) Логирование

### 13.1 Требование по формату и именованию

Формат и ротация логов должны соответствовать формату логов сервера. Хороший пример — дата в имени файла в ISO-формате (YYYY-MM-DD).

### 13.2 Что логировать

- Все сообщения чатов
- Все PM
- Сообщения, заблокированные:
    
    
    - validation
    - similarity
    - permissions/прочие фильтры

### 13.3 Где логировать

- Server/World/Plot — на Paper
- Network Global — на Velocity
- PM — на Velocity

### 13.4 Минимальная структура

Должны быть минимум два потока:

- разрешённые сообщения
- заблокированные сообщения

Пример (точное имя/папка — по стандарту сервера):

- `logs/colombinochat/2026-02-22-chat.log`
- `logs/colombinochat/2026-02-22-chat-blocked.log`

---

## 14) Permissions (минимум)

Чаты:

- `colombinochat.chat.network`
- `colombinochat.chat.server`
- `colombinochat.chat.world`
- `colombinochat.chat.plot`

PM:

- `colombinochat.pm.send`
- `colombinochat.pm.reply`
- `colombinochat.pm.ignore`

Сообщения:

- `colombinochat.message.minimessage`
- `colombinochat.message.links`

Bypass:

- `colombinochat.validation.bypass`
- `colombinochat.similarity.bypass`

Mentions (опционально):

- `colombinochat.mentions.bypass`

SocialSpy (опционально):

- `colombinochat.socialspy`

---

## 15) Hot Reload

Команда:

- `/colombinochat reload`

Требования:

- reload перезагружает: форматы, messages, validation, similarity, mentions
- предусмотреть отдельную подкоманду для работы с БД:
    
    
    - `/colombinochat reload db` — переподключение/проверка соединения, обновление кэшей (если используются)
- алиасы команд/регистрация новых команд может требовать рестарт (допускается)

---

## 16) Acceptance Criteria (готовность)

- Работают 4 scope: network/server/world/plot (форматы + триггеры)
- Активный scope переключается и сохраняется согласно настройкам (если включено)
- PM/Reply/Ignore работают по сети Velocity и используют PostgreSQL
- Mentions работают со звуком и кулдауном; оффлайн-упоминания доставляются при входе и хранятся в PostgreSQL
- Validation и Similarity корректно блокируют сообщения + уведомляют отправителя
- Linkify работает только при permission
- Логи пишутся на корректной стороне (paper/velocity), имена/даты соответствуют формату логов сервера, есть blocked лог

### Опциональные интеграции / отложено

**A) Реплейсер строк (Text Replacer) — опционально**

Фича может быть добавлена позже при необходимости.

Пример конфига:

```yaml
replacer:
  enabled: true
  mappings:
    ":doge:": "☺"
    ":lol:": "☻"

```

**B) Discord Bridge — отложено**

Discord bridge исключён из текущего объёма работ, т.к. требует интеграции с системой верификации и наличия у неё API.

Вернуться к задаче после утверждения:

- модели верификации
- API/протокола взаимодействия бота и прокси
- требований по безопасности и анти-эхо

# Метки на плотах — ТЗ (идея)

<p class="callout info">Проект Plane: <strong>PLOTMARK</strong> (из CREATIVE-20). Созвон 20.08.2026 + фидбэк в Discord «взаимодействие с плотами.» Документ фиксирует продуктовую идею, не финальный контракт реализации.</p>

<blockquote id="bkmrk-summary">
<p>Staff ставит на плоту заметную метку в точке правки с текстом. Игрок кликает → Dialog с запиской → закрывает метку по правилам режима. Автор получает уведомление. Менять блоки на чужом плоту ради подсказки <strong>не нужно</strong>.</p>
</blockquote>

<hr class="separator" id="bkmrk-sep-1">

<h2 id="bkmrk-1-goal">1) Зачем</h2>
<p id="bkmrk-1-p">Подсказать «где и что поправить», в том числе когда игрок оффлайн, без правок его постройки. Задача staff — довести игрока до самостоятельной правки, а не править за него.</p>

<hr class="separator" id="bkmrk-sep-2">

<h2 id="bkmrk-2-modes">2) Режимы и права</h2>
<table id="bkmrk-modes">
<thead>
<tr><th>Режим</th><th>Кто ставит</th><th>Поведение игрока</th></tr>
</thead>
<tbody>
<tr><td><strong>Примечание</strong></td><td>Helper и Mentor</td><td>Может убрать сам, без подтверждения автора</td></tr>
<tr><td><strong>Критическая</strong></td><td>Только Mentor (+ Senior Mentor)</td><td>Нельзя просто отклонить; принять как выполненную или <strong>оспорить</strong></td></tr>
</tbody>
</table>

<ul id="bkmrk-limits">
<li><p><strong>Лимиты на плот одновременно:</strong> Helper — до 3 примечаний; Mentor — до 10 меток любого режима.</p></li>
<li><p>Создание — по LuckPerms. Наказаний за «принял и не сделал» нет.</p></li>
</ul>

<p class="callout info">На созвоне обсуждали также «предложение / предупреждение / обязательная». Рабочая модель из staff-фидбэка — два режима выше; при необходимости «предупреждение» можно добавить позже как третий.</p>

<hr class="separator" id="bkmrk-sep-3">

<h2 id="bkmrk-3-flows">3) Сценарии</h2>
<ol id="bkmrk-flows">
<li><p><strong>Создание:</strong> команда на точке плота → Dialog (текст + режим) → метка появляется.</p></li>
<li><p><strong>Игрок:</strong> клик по метке → Dialog с запиской → принять / убрать / оспорить (по режиму).</p></li>
<li><p><strong>Уведомления:</strong> автору — о действии игрока (онлайн или при входе); игроку при входе — если появились новые метки.</p></li>
<li><p><strong>Списки:</strong> staff — «мои / открытые» + телепорт к метке; игрок — меню по своим плотам (PlotSquared API).</p></li>
</ol>

<hr class="separator" id="bkmrk-sep-4">

<h2 id="bkmrk-4-visual">4) Визуал (исследовать)</h2>
<ul id="bkmrk-visual">
<li><p>Заметный символ «!» (голограмма / головы / atlas sprites), подпись «нажмите».</p></li>
<li><p>Разный вид для примечания и критической.</p></li>
<li><p>Референс: <a href="https://www.gamergeeks.net/apps/minecraft/list-of-atlas-sprites/map-decorations">map decorations atlas sprites</a>.</p></li>
</ul>

<hr class="separator" id="bkmrk-sep-5">

<h2 id="bkmrk-5-stack">5) Техрамка</h2>
<ul id="bkmrk-stack">
<li><p>Paper (Creative): PlotSquared API, Dialog API, LuckPerms, Adventure/MiniMessage.</p></li>
<li><p>Опционально FancyHolograms / display entities — по прототипу.</p></li>
<li><p>БД, точные permission-ноды и имена команд — в детальном ТЗ после MVP-решения.</p></li>
</ul>

<hr class="separator" id="bkmrk-sep-6">

<h2 id="bkmrk-6-out">6) Вне скоупа</h2>
<ul id="bkmrk-out">
<li><p>Права хелпера строить/ломать на чужом плоту без согласия.</p></li>
<li><p>«Ветки» правок staff с merge/reject блоками.</p></li>
<li><p>Глобальная панель «все игроки с недоделками» (достаточно списка меток + TP).</p></li>
</ul>
```

# Colombino Analytics — ТЗ

<p class="callout info" id="bkmrk-plane">Проект Plane: пока <strong>CREATIVE</strong> (отдельный проект аналитики не заведён). Источник: обсуждение 25.08.2026 (retention, ключевые действия, история плотов, Plan). Документ — контракт v1, не экран в Plan.</p>
<blockquote id="bkmrk-summary">
<p>Плагин-шина ключевых действий игроков. Другие плагины вызывают API, события пишутся в <strong>свою PostgreSQL</strong>. Plan (<a href="https://stats.binomc.net">stats.binomc.net</a>), DataExtension и отчёт воронки в v1 <strong>нет</strong>.</p>
</blockquote>
<hr class="separator" id="bkmrk-sep-0">
<h2 id="bkmrk-0-tech">0) Техническая информация</h2>
<ul id="bkmrk-0-list">
<li>
<p><strong>Название:</strong> <code>ColombinoAnalytics</code></p>
</li>
<li>
<p><strong>Платформа:</strong> Paper (Hub, Creative, ивентовые) + Velocity <strong>Master</strong></p>
</li>
<li>
<p><strong>Сборка:</strong> один репозиторий, три модуля:</p>
<ul>
<li>
<p><code>analytics-api</code> — тонкий jar, <code>compileOnly</code> у других плагинов</p>
</li>
<li>
<p><code>analytics-paper</code></p>
</li>
<li>
<p><code>analytics-velocity</code></p>
</li>
</ul>
</li>
<li>
<p><strong>БД:</strong> отдельная database PostgreSQL (тот же хост, что Plane/Bookstack, <strong>не</strong> их схемы и <strong>не</strong> БД Plan)</p>
</li>
<li>
<p><strong>Soft-depend (Paper):</strong> PlotSquared, BinoRankup, PlotMark — нет плагина, нет слушателя, API живёт</p>
</li>
</ul>
<hr class="separator" id="bkmrk-sep-1">
<h2 id="bkmrk-1-why">1) Зачем</h2>
<p id="bkmrk-1-p">Три цели на одном логе. Creative — UGC: активация не «зашёл», а <strong>взял слот</strong>. Ивентовые режимы (Build Battle и др.) в логе не отдельные продукты, а значение <code>server</code> у захода.</p>
<table id="bkmrk-1-goals">
<thead>
<tr>
<th>Цель</th>
<th>Что даёт лог</th>
<th>Когда на экране</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Шина</strong></td>
<td>API для других плагинов</td>
<td>v1</td>
</tr>
<tr>
<td><strong>Модерация</strong></td>
<td>История владения плотом</td>
<td>данные в v1, UI позже</td>
</tr>
<tr>
<td><strong>Воронка новичка</strong></td>
<td>Где обрыв до активации и ранга</td>
<td>данные в v1, отчёт позже</td>
</tr>
<tr>
<td><strong>Staff</strong></td>
<td>Ревью, staff-rate, марки PlotMark</td>
<td>данные в v1, UI позже</td>
</tr>
</tbody>
</table>
<hr class="separator" id="bkmrk-sep-2">
<h2 id="bkmrk-2-done">2) Готово когда (v1)</h2>
<ol id="bkmrk-2-ol">
<li>
<p>Jar <code>analytics-api</code>: другой плагин вызывает <code>track</code>, строка появляется в Postgres. Если analytics не установлен — вызов no-op, не падает.</p>
</li>
<li>
<p>Velocity <strong>Master</strong> пишет заходы с каноническим именем сервера.</p>
</li>
<li>
<p>Paper <strong>Creative</strong> пишет плоты, заявки ранга, staff-ревью/рейтинг, марки PlotMark.</p>
</li>
<li>
<p>Paper стоит на <strong>Hub</strong> и <strong>ивентовых</strong> бэкендах (API и будущие типы; заходы в v1 пишет прокси).</p>
</li>
</ol>
<p class="callout warning" id="bkmrk-2-out">Не в приёмке v1: DataExtension и страницы Plan, дашборд retention, <code>BlockPlace</code>, плагин на Shadow, TTL/чистка лога, Redis.</p>
<hr class="separator" id="bkmrk-sep-3">
<h2 id="bkmrk-3-terms">3) Термины</h2>
<table id="bkmrk-3-table">
<thead>
<tr>
<th>Термин</th>
<th>Смысл</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Network-сессия</strong></td>
<td>Пока игрок в сети (Velocity), не «пока на этом Paper»</td>
</tr>
<tr>
<td><strong>Канон <code>server</code></strong></td>
<td>Имя сервера как в Velocity: <code>hub</code>, <code>creative</code>, имя ивентового</td>
</tr>
<tr>
<td><strong>Активация</strong></td>
<td><code>plot.claimed</code> — взял слот, не заход на Creative</td>
</tr>
<tr>
<td><strong>DataExtension</strong></td>
<td>API Plan для <em>снимка</em> на сайте. Лог не пишет. В v1 не используем</td>
</tr>
<tr>
<td><strong>Query API Plan</strong></td>
<td>SQL в БД Plan. Типы движка: SQLite или MySQL. Postgres через Plan нет. В v1 не используем</td>
</tr>
</tbody>
</table>
<hr class="separator" id="bkmrk-sep-4">
<h2 id="bkmrk-4-arch">4) Архитектура</h2>
<p id="bkmrk-4-why-not-plan"><strong>Почему не таблица в БД Plan:</strong> DataExtension не пишет события (4 колонки на игрока, 50 символов в ячейке). Query API — SQLite/MySQL. Заходы со всех серверов не кладём в боевую БД Plan: патчи, <code>/plan db clear</code> и <code>move</code> не знают чужие таблицы; воронки будут конкурировать с вебом.</p>
<p id="bkmrk-4-flow">Поток: Velocity Master и Paper пишут в одну PostgreSQL <code>player_events</code>. Прокси шлёт на Paper plugin message канала <code>colombino:analytics</code> (<code>session_id</code> + канон <code>server</code>). Плот, ранг и марки прокси <strong>не</strong> слушает.</p>
<hr class="separator" id="bkmrk-sep-5">
<h2 id="bkmrk-5-funnel">5) Воронка новичка (как смотрит аналитик)</h2>
<ol id="bkmrk-5-ol">
<li>
<p>Зашёл в сеть → заход с <code>server=hub</code></p>
</li>
<li>
<p>Попал на Creative → заход с <code>server=creative</code></p>
</li>
<li>
<p>Взял плот → <code>plot.claimed</code> ← <strong>активация</strong></p>
</li>
<li>
<p>Подал <code>/done</code> → <code>rank.request_submitted</code> (<code>/rankup</code> — только меню, не этот шаг)</p>
</li>
<li>
<p>Заявка закрылась → <code>rank.request_status_changed</code> (<code>APPROVED</code> / <code>REJECTED</code> / <code>WITHDRAWN</code> / <code>DELETED</code>)</p>
</li>
<li>
<p>Получил ранг → <code>rank.granted</code></p>
</li>
</ol>
<p class="callout warning" id="bkmrk-5-hole"><strong>Дыра между 3 и 4:</strong> PlotSquared не видит «начал строить». Без <code>BlockPlace</code> на своём плоте не отличить «взял и бросил» от «строит третий день». В v1 дыру помечаем, не закрываем.</p>
<p id="bkmrk-5-bb">«Поучаствовал в Build Battle» = заход на ивентовый сервер. Отдельного типа <code>build_battle.joined</code> нет.</p>
<hr class="separator" id="bkmrk-sep-6">
<h2 id="bkmrk-6-catalog">6) Каталог событий v1 (закрыт)</h2>
<p id="bkmrk-6-intro">Строка: <code>player_uuid</code>, <code>event_type</code>, <code>occurred_at</code>, <code>server</code>, <code>session_id</code>, <code>extra</code> (JSONB строго по типу). Неизвестный тип API отвергает. Свободных строк типов нет.</p>
<h3 id="bkmrk-6-1-joins">6.1 Заходы (пишет только Velocity)</h3>
<p id="bkmrk-6-1-p">Hub → Creative → Hub → Creative в одном визите: по одному <code>session_joined</code> на <code>hub</code> и на <code>creative</code>. Возврат на Creative в той же сессии не пишем.</p>
<table id="bkmrk-6-1-table">
<thead>
<tr>
<th>Тип</th>
<th>Смысл</th>
<th>Extra</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>player.first_joined_server</code></td>
<td>Первый раз в жизни на этом <code>server</code></td>
<td><code>server</code></td>
</tr>
<tr>
<td><code>player.session_joined_server</code></td>
<td>Первый раз на этом <code>server</code> в network-сессии</td>
<td><code>server</code></td>
</tr>
</tbody>
</table>
<h3 id="bkmrk-6-2-plots">6.2 Плоты (PlotSquared, Creative)</h3>
<p id="bkmrk-6-2-p">Активация и история владения. <code>PlayerEnterPlotEvent</code> не пишем. Trusted/members в v1 нет. <code>plot_id</code> — идентификатор из PlotSquared API. <code>width</code> — ширина в блоках.</p>
<table id="bkmrk-6-2-table">
<thead>
<tr>
<th>Тип</th>
<th>Источник</th>
<th>Extra</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>plot.claimed</code></td>
<td><code>PlotClaimedNotifyEvent</code> (<code>wasAuto()</code> → <code>method</code>: <code>auto</code> | <code>claim</code>)</td>
<td><code>plot_id</code>, <code>world</code>, <code>width</code>, <code>method</code></td>
</tr>
<tr>
<td><code>plot.unclaimed</code></td>
<td>unclaim</td>
<td><code>plot_id</code>, <code>world</code>, <code>width</code></td>
</tr>
<tr>
<td><code>plot.deleted</code></td>
<td>delete</td>
<td><code>plot_id</code>, <code>world</code>, <code>width</code></td>
</tr>
<tr>
<td><code>plot.cleared</code></td>
<td>clear</td>
<td><code>plot_id</code>, <code>world</code>, <code>width</code></td>
</tr>
<tr>
<td><code>plot.transferred</code></td>
<td><code>PlotChangeOwnerEvent</code></td>
<td>то же + <code>from_uuid</code>, <code>to_uuid</code></td>
</tr>
</tbody>
</table>
<h3 id="bkmrk-6-3-rank-player">6.3 Ранг — новичок (BinoRankup)</h3>
<p id="bkmrk-6-3-p">Слушаем Bukkit API плагина, хуки не выдумываем. <code>categories</code> — как отдаёт Rankup (JSON-массив строк). Имена классов сверить с репозиторием при реализации; смысл событий не менять.</p>
<table id="bkmrk-6-3-table">
<thead>
<tr>
<th>Тип</th>
<th>Слушаем</th>
<th>Extra</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>rank.request_submitted</code></td>
<td><code>RequestSubmitEvent</code></td>
<td><code>request_id</code>, <code>from_rank</code>, <code>to_rank</code>, <code>plot_id</code>, <code>world</code>, <code>width</code>, <code>categories</code></td>
</tr>
<tr>
<td><code>rank.request_status_changed</code></td>
<td><code>RequestStatusChangedEvent</code></td>
<td><code>request_id</code>, <code>from_status</code>, <code>to_status</code></td>
</tr>
<tr>
<td><code>rank.granted</code></td>
<td><code>PlayerRankGrantedEvent</code></td>
<td><code>from_rank</code>, <code>to_rank</code>, <code>request_id</code></td>
</tr>
</tbody>
</table>
<h3 id="bkmrk-6-4-rank-staff">6.4 Ранг — staff (основная метрика команды)</h3>
<table id="bkmrk-6-4-table">
<thead>
<tr>
<th>Тип</th>
<th>Слушаем</th>
<th>Extra</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>rank.review_submitted</code></td>
<td><code>ReviewSubmitEvent</code></td>
<td><code>request_id</code>, <code>vote</code> (<code>APPROVE</code>/<code>REJECT</code>), <code>voter_role</code> как в конфиге Rankup, <code>plot_id</code>, <code>world</code></td>
</tr>
<tr>
<td><code>rank.rating_submitted</code></td>
<td><code>RatingSubmitEvent</code> только если у <strong>голосующего</strong> роль Helper / Mentor / Senior Mentor</td>
<td><code>request_id</code>, <code>plot_id</code>, <code>world</code></td>
</tr>
</tbody>
</table>
<p id="bkmrk-6-4-note">Review без <code>voter_role</code> не пишем: вес голоса в Rankup от роли. Обычный игроковый <code>/rate</code> в лог не идёт (отсекаем на входе).</p>
<h3 id="bkmrk-6-5-marks">6.5 Марки (PlotMark, Creative)</h3>
<p id="bkmrk-6-5-p">См. <a href="https://wiki.binomc.net/books/other/page/metki-na-plotax-tz-ideia">Метки на плотах — ТЗ</a>. PlotMark вызывает <code>Analytics.track</code> или отдаёт Bukkit-событие с тем же extra.</p>
<table id="bkmrk-6-5-table">
<thead>
<tr>
<th>Тип</th>
<th>Extra</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>staff.mark_placed</code></td>
<td><code>mark_type</code>: <code>pointer</code> / <code>edit</code> / <code>rework</code>; <code>plot_id</code>, <code>world</code></td>
</tr>
<tr>
<td><code>staff.mark_resolved</code></td>
<td>то же + <code>resolved_by</code>: <code>player</code> | <code>staff</code></td>
</tr>
</tbody>
</table>
<hr class="separator" id="bkmrk-sep-7">
<h2 id="bkmrk-7-storage">7) Хранение</h2>
<p id="bkmrk-7-p">Таблица <code>player_events</code>, append-only.</p>
<table id="bkmrk-7-cols">
<thead>
<tr>
<th>Колонка</th>
<th>Тип</th>
<th>Назначение</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>id</code></td>
<td><code>bigserial</code></td>
<td>PK</td>
</tr>
<tr>
<td><code>occurred_at</code></td>
<td><code>timestamptz</code></td>
<td>время события, не вставки</td>
</tr>
<tr>
<td><code>received_at</code></td>
<td><code>timestamptz</code></td>
<td>время записи в Postgres</td>
</tr>
<tr>
<td><code>player_uuid</code></td>
<td><code>uuid</code></td>
<td>игрок</td>
</tr>
<tr>
<td><code>event_type</code></td>
<td><code>text</code></td>
<td>только каталог §6</td>
</tr>
<tr>
<td><code>server</code></td>
<td><code>text</code></td>
<td>канон Velocity</td>
</tr>
<tr>
<td><code>session_id</code></td>
<td><code>uuid</code> nullable</td>
<td>network-сессия; null если прокси не доставил</td>
</tr>
<tr>
<td><code>extra</code></td>
<td><code>jsonb</code></td>
<td>контракт типа</td>
</tr>
</tbody>
</table>
<p id="bkmrk-7-idx"><strong>Индексы:</strong> <code>(player_uuid, occurred_at DESC)</code>; <code>(event_type, occurred_at)</code>; <code>(server, event_type, occurred_at)</code>; expression <code>(extra->>'plot_id')</code>, <code>(extra->>'request_id')</code>. GIN по всему extra в v1 нет.</p>
<p id="bkmrk-7-uniq"><strong>Уникальность:</strong> <code>first_joined</code> — <code>(player_uuid, event_type, server)</code>. <code>session_joined</code> — <code>(session_id, event_type, server) WHERE session_id IS NOT NULL</code> (без <code>WHERE</code> в Postgres несколько NULL не дедупятся). Повтор — <code>ON CONFLICT DO NOTHING</code>. Плот/ранг/марки не дедупим.</p>
<p id="bkmrk-7-ttl">TTL сырого лога в v1 нет. Политика хранения — отдельное решение по факту объёма.</p>
<hr class="separator" id="bkmrk-sep-8">
<h2 id="bkmrk-8-roles">8) Кто пишет что</h2>
<h3 id="bkmrk-8-1-vel">8.1 Velocity Master</h3>
<ul id="bkmrk-8-1-ul">
<li>
<p>Выдаёт <code>session_id</code> на время пребывания в сети.</p>
</li>
<li>
<p>Пишет два типа заходов (§6.1). First: кэш + уникальный индекс (гонка → conflict ignore).</p>
</li>
<li>
<p>После коннекта/switch на Paper — plugin message с <code>session_id</code> и каноном <code>server</code>.</p>
</li>
</ul>
<h3 id="bkmrk-8-2-paper">8.2 Paper</h3>
<ul id="bkmrk-8-2-ul">
<li>
<p>Не считает first/session. <code>track()</code> дописывает текущие <code>session_id</code> и <code>server</code>.</p>
</li>
<li>
<p>Нет сообщения с прокси — событие пишем, <code>session_id = null</code>. Факт <code>plot.claimed</code> важнее склейки с сессией.</p>
</li>
<li>
<p>Состояние: карта UUID → сессия до quit. Тот же <code>session_id</code> при каждом switch.</p>
</li>
<li>
<p>Гонка: событие раньше plugin message — допустимо, <code>session_id</code> null.</p>
</li>
</ul>
<p class="callout info" id="bkmrk-8-shadow"><strong>Shadow:</strong> в v1 не ставим. Иначе два источника сессий. Игрок через Shadow = как без прокси.</p>
<p id="bkmrk-8-no">Не в v1: Redis, слушатели плотов на прокси.</p>
<hr class="separator" id="bkmrk-sep-9">
<h2 id="bkmrk-9-api">9) API</h2>
<pre id="bkmrk-9-sig"><code>boolean track(UUID player, String eventType, Map&lt;String, Object&gt; extra)</code></pre>
<ul id="bkmrk-9-ul">
<li>
<p>Нет paper/velocity плагина на сервере → no-op, <code>false</code>.</p>
</li>
<li>
<p>Тип не из каталога или нет обязательных ключей extra → <code>false</code>, warn в лог, в очередь не кладём.</p>
</li>
<li>
<p>Успешный enqueue → <code>true</code> (ещё не значит «уже в Postgres»).</p>
</li>
</ul>
<p id="bkmrk-9-thread">Игровой поток только enqueue. Фоновый writer — batch insert. Главный поток JDBC не ждёт.</p>
<hr class="separator" id="bkmrk-sep-10">
<h2 id="bkmrk-10-errors">10) Ошибки</h2>
<table id="bkmrk-10-table">
<thead>
<tr>
<th>Ситуация</th>
<th>Поведение</th>
</tr>
</thead>
<tbody>
<tr>
<td>Postgres недоступен при старте</td>
<td>Плагин включается; <code>track</code> enqueue; периодический warn</td>
</tr>
<tr>
<td>Postgres лёг в рантайме</td>
<td>Очередь в памяти, потолок <strong>10 000</strong>. Overflow: drop + счётчик. Игрок не лагает</td>
</tr>
<tr>
<td>После подъёма БД</td>
<td>Flush очереди</td>
</tr>
<tr>
<td>Failover на диск/SQLite</td>
<td>Нет в v1 (два источника правды)</td>
</tr>
<tr>
<td>Битый plugin message</td>
<td>Игнор</td>
</tr>
<tr>
<td>Неверный extra / тип</td>
<td>Отказ до очереди</td>
</tr>
</tbody>
</table>
<hr class="separator" id="bkmrk-sep-11">
<h2 id="bkmrk-11-tests">11) Тесты (CI без живого MC)</h2>
<ul id="bkmrk-11-ul">
<li>
<p>Контракт extra: полный набор — ok; дырявый — reject.</p>
</li>
<li>
<p>Дедуп <code>first_joined</code> и <code>session_joined</code>.</p>
</li>
<li>
<p><code>rank.rating_submitted</code>: Helper/Mentor/Senior Mentor — пишем; игрок без роли — нет.</p>
</li>
<li>
<p><code>rank.review_submitted</code> без <code>voter_role</code> — нет.</p>
</li>
<li>
<p>Запрос-заготовка воронки: цепочка типов по uuid (не UI).</p>
</li>
</ul>
<hr class="separator" id="bkmrk-sep-12">
<h2 id="bkmrk-12-next">12) Следующие этапы (не v1)</h2>
<ol id="bkmrk-12-ol">
<li>
<p>DataExtension Plan: счётчики, first/last, урезанная история (лимиты Plan: 4 колонки, 50 символов).</p>
</li>
<li>
<p>Отчёт воронки / retention по когортам (SQL или отдельный сервис).</p>
</li>
<li>
<p>Закрытие дыры «взял плот, не строит».</p>
</li>
<li>
<p>Velocity Shadow, если станет основным входом.</p>
</li>
<li>
<p>TTL/партиции, когда будет ясен объём.</p>
</li>
</ol>
<hr class="separator" id="bkmrk-sep-13">
<h2 id="bkmrk-13-rejected">13) Сознательно отвергнутое</h2>
<ul id="bkmrk-13-ul">
<li>
<p>Хранение в БД Plan через Query API.</p>
</li>
<li>
<p>DataExtension как «запись в БД» — он не пишет лог.</p>
</li>
<li>
<p><code>build_battle.joined</code> — дубль захода на ивентовый <code>server</code>.</p>
</li>
<li>
<p><code>PlayerEnterPlotEvent</code> — ходьба, не ключ.</p>
</li>
<li>
<p>Каждый Paper join без дедупа по network-сессии.</p>
</li>
<li>
<p>PostgreSQL через Plan API — не поддерживается (<code>SQLITE</code> / <code>MYSQL</code> only).</p>
</li>
</ul>
```