L2 Market Parser
Кроссплатформенный локальный прототип для захвата нескольких окон Lineage II и сбора предметов из tooltip торгового окна.
Прогресс иконок магазина
Для жёлто-зелёного прогресса дополнительно выдели области Сетка предметов Sell и Сетка предметов Buy. Нужен только прямоугольник 6 × 3 с ячейками предметов, без заголовка и кнопок прокрутки. Поля необязательны, поэтому старые калибровки продолжают распознавать текст без оверлея иконок.
На первом чистом кадре магазина занятые ячейки становятся жёлтыми. После успешного OCR и однозначного сравнения с иконкой из каталога соответствующая ячейка становится зелёной. При одинаково подходящих иконках ячейка остаётся жёлтой; приложение не угадывает слот по положению tooltip или порядку обхода. Выбранный источник разворачивается минимум до 640 CSS px, а экран калибровки показывает исходный скриншот в масштабе 2×, сохраняя координаты в исходных пикселях.
Проверка включает обычные unit-тесты, 17 подписанных игровых скриншотов в Chromium, production build и проверку diff:
npm run check
Запуск
Нужен Node.js 20 или новее и Chrome либо Edge.
npm install
cp .env.local.example .env.local
npm run dev
Открой адрес, который напечатает Vite, обычно http://127.0.0.1:5173.
В основном приложении этот же интерфейс доступен в Рынок → Аудит рынка. Там сохранённая калибровка общая: сервер загружает её при открытии вкладки и заменяет после нажатия «Сохранить».
На macOS браузеру потребуется разрешение System Settings → Privacy & Security → Screen & System Audio Recording. После выдачи разрешения браузер иногда нужно перезапустить.
Как работает поиск
Основное торговое окно считается единым объектом. Пользователь отмечает его нижний постоянный фрагмент, а приложение ищет этот фрагмент по всему кадру. Когда фрагмент найден, его координаты становятся точкой отсчёта для заголовка магазина и зоны tooltip.
Кадр
└─ Маркер магазина найден?
├─ нет → заголовок и tooltip не проверяются
└─ да
├─ OCR `Private Store(Buy/Sell) - имя`
│ ├─ виден → запомнить тип сделки + имя торговца
│ └─ закрыт tooltip → использовать последний запомненный магазин
└─ поиск Price : внутри зоны tooltip
├─ нет → название и цена не читаются
└─ да → OCR точной строки названия и точной строки цены
Заголовок и зона tooltip хранятся относительно маркера магазина. Поэтому всё торговое окно можно перемещать. Широкая зона tooltip используется только для поиска Price : и не передаётся целиком в OCR. После нахождения якоря приложение отдельно определяет фактическую ширину строк названия и цены по пикселям текста; для названия учитываются белый, синий и фиолетовый цвета.
Из заголовка Private Store(Buy) - Hikvision получится side: "buy", merchant: "Hikvision"; из Private Store(Sell) - Dwa — side: "sell", merchant: "Dwa".
Контекст хранится отдельно для каждого захватываемого окна. Пока нижний маркер магазина остаётся виден, tooltip относится к последнему успешно распознанному заголовку этого окна. После трёх кадров без маркера контекст сбрасывается, чтобы предметы следующего магазина не попали в предыдущий. Если заголовок ещё ни разу не распознан, предмет из tooltip не сохраняется.
Сценарий
- В разделе «Экран» нажми «Добавить окно» и выбери окно игры. Приложение сразу откроет «Парсинг».
- Нажми «Начать парсинг» и вернись в игру.
- Сначала оставь курсор вне предметов, пока не появится блок торговца с типом сделки и именем.
- Наводи курсор на каждый предмет. Товары заполняют блок торговца, следующий торговец появляется справа или на новой строке.
- Калибровку открывай как дополнительную настройку, если текущая общая конфигурация не распознаёт окно или tooltip.
Новое наблюдение сначала попадает в IndexedDB браузера и сразу отправляется на сервер; 30-секундный повтор остаётся страховкой. При недоступном сервере пакет остаётся локально и повторяется с тем же ID после восстановления связи или перезапуска страницы. Сервер хранит исходный пакет без дублей и для распознанных каталожных предметов обновляет отдельный черновик рынка: Sell становится sell_offer, Buy становится buy_order. Неопознанные товары остаются только в исходном пакете.
Калибровку можно сделать по снимкам разного размера и с торговым окном в разных местах:
- Без tooltip выдели нижний маркер и заголовок
Private Store(Buy/Sell) - имя. - Добавь второй снимок с tooltip. Калибратор сам найдёт нижний маркер по всему новому кадру и перенесёт относительно него остальные области.
- На втором снимке выдели зону tooltip,
Price :, название и цену. Уже выбранные области сохраняются.
Размер игрового окна и положение торгового окна могут отличаться. В калибраторе снимок показывается в масштабе 2× для точной ручной разметки, но шаблоны и сохранённые координаты остаются в исходном пиксельном размере Lineage II.
После выбора всех шести областей нажми «Проверить снимки». Они обрабатываются по порядку: сначала кадр с видимым заголовком запоминает магазин, затем кадр с tooltip добавляет предмет. Подключать игровое окно для этого не нужно.
Результат состоит только из блоков торговцев. В каждом блоке показаны тип сделки и последние распознанные цена/количество каждого предмета. Количество читается из числовых скобок в конце названия, например (18) или (5,600); если их нет, используется 1. Повторное распознавание той же комбинации не создаёт новую запись.
Во вкладке приложения OCR-название проходит неточный поиск по активному каталогу. Уверенное совпадение заменяется каноническим названием и показывается с иконкой предмета; слабое совпадение остаётся с вопросительным знаком и пометкой «Не найден в каталоге». Предметы группируются по имени торговца и типу сделки.
Количеством считается только последняя числовая скобка. В Blessed Spiritshot: D-Grade (D) (5,600) часть (D) остаётся в названии, а количество равно 5600.
Если рамки перекрывают соседние элементы, выключи «Показывать выбранные рамки». Сохранённые области не удалятся, а новый пунктирный прямоугольник останется виден во время выделения.
Что именно выделять
| Область | Правильный выбор | Частая ошибка |
|---|---|---|
| Маркер магазина | Небольшой постоянный фрагмент внизу, например Adena и Confirm |
Захватить баланс Adena или другое изменяемое число |
| Заголовок магазина | Вся строка Private Store(Sell) - Dwa или Private Store(Buy) - Hikvision |
Обрезать Buy/Sell или имя; захватить рамку и кнопку закрытия |
| Зона tooltip | Широкая полоса возможных положений всплывающего окна, в том числе за пределами окна трейда | Включить нижнюю строку Price основного окна |
| Якорь Price | Только постоянные символы Price : внутри tooltip |
Добавить цену, которая меняется у каждого предмета |
| Название предмета | Левый край и точная высота верхней строки tooltip; ширина подгоняется автоматически | Захватить иконки, фон игры или строку цены |
| Строка цены | Левый край и точная высота Price : 3,000,000 Adena, For Each 400 Adena или числа |
Захватить дополнительную строку (3 Million Adena) |
Калиброваться на самом длинном предмете больше не требуется: после поиска Price : границы названия и цены подгоняются отдельно и останавливаются до удалённого текста интерфейса.
Диагностика
В результате показывается последний обработанный кадр с рамками и каждый этап отдельно:
- зелёная рамка означает, что шаблон прошёл заданный порог;
- жёлтая показывает найденную область предмета, которую OCR ещё не разобрал;
- красная показывает лучшее совпадение, которое не прошло порог;
- синяя показывает вычисленную область следующего шага;
- «Пропущено» означает, что предыдущий обязательный этап не прошёл;
- рядом с OCR показываются фактический crop, текст и уверенность.
Если не находится магазин, смотри crop «Маркер магазина» и процент совпадения. Если тип или имя ещё не определены, убери курсор с предмета и смотри фактический OCR этапа «Заголовок магазина». Надпись «Используется последний магазин» означает, что tooltip закрыл заголовок, но привязка предмета сохранена. Если магазин найден, но предмет не появляется, смотри «Якорь Price :». Если оба якоря найдены, проблема уже в точных crop названия или цены.
Диагностические изображения существуют только в памяти открытой страницы. Они не попадают в JSON результата и не отправляются вместе с рыночными наблюдениями.
В отдельном Vite-прототипе калибровка хранится в localStorage. Во вкладке home-service серверная калибровка является основной, а localStorage остаётся локальной резервной копией. При первом OCR Tesseract.js загружает английскую языковую модель и кеширует её в браузере.
Ограничения прототипа
- Источники выбираются вручную после каждого обновления страницы.
- Одна калибровка рассчитана на один масштаб интерфейса Lineage II.
- Парсинг работает, пока открыта страница прототипа и он не остановлен кнопкой «Остановить парсинг».
- Курсор перемещает пользователь, приложение не управляет игрой и не эмулирует ввод.
- Токен импорта хранится только в локальном Vite-процессе; в браузерный bundle он не попадает.
Проверки
npm test
npm run build
# Обновить встроенную копию в соседнем home-service:
npm run build -- --outDir ../home-service/frontend/public/l2/market-audit --emptyOutDir