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) - Dwaside: "sell", merchant: "Dwa".

Контекст хранится отдельно для каждого захватываемого окна. Пока нижний маркер магазина остаётся виден, tooltip относится к последнему успешно распознанному заголовку этого окна. После трёх кадров без маркера контекст сбрасывается, чтобы предметы следующего магазина не попали в предыдущий. Если заголовок ещё ни разу не распознан, предмет из tooltip не сохраняется.

Сценарий

  1. В разделе «Источники» нажми «Добавить окно» для каждого окна игры.
  2. В разделе «Калибровка» сделай снимок или загрузи готовый скриншот.
  3. Выдели постоянный нижний элемент торгового окна, например Adena + Confirm, как маркер магазина. По возможности не захватывай изменяемые числа.
  4. Выдели заголовок магазина одной строкой: от Private Store( до конца имени торговца. Рамку и кнопку закрытия не включай.
  5. Выдели широкую горизонтальную полосу всех возможных положений tooltip. Она может выходить далеко за пределы торгового окна, но не должна захватывать нижний Price основного окна.
  6. Внутри tooltip обведи только постоянную надпись Price :.
  7. Отдельно выдели левый край, высоту и начальную ширину строки названия и строки цены. Runtime сам продлит каждую строку вправо до фактического конца текста.
  8. Сохрани калибровку, нажми «Начать сбор» и вернись в игру.
  9. Сначала оставь курсор вне предметов, пока в результате не появятся тип сделки и имя торговца.
  10. Наводи курсор на каждый предмет. Даже если tooltip закроет заголовок, предмет будет записан в последний найденный магазин.

Наблюдения сначала попадают в IndexedDB браузера, затем отправляются пакетами раз в 30 секунд. При недоступном сервере пакет остаётся локально и повторяется с тем же ID после восстановления связи или перезапуска страницы; сервер не создаёт дубликат.

Калибровку можно сделать по снимкам разного размера и с торговым окном в разных местах:

  1. Без tooltip выдели нижний маркер и заголовок Private Store(Buy/Sell) - имя.
  2. Добавь второй снимок с tooltip. Калибратор сам найдёт нижний маркер по всему новому кадру и перенесёт относительно него остальные области.
  3. На втором снимке выдели зону tooltip, Price :, название и цену. Уже выбранные области сохраняются.

Размер игрового окна и положение торгового окна могут отличаться. В калибраторе снимок показывается в масштабе 2× для точной ручной разметки, но шаблоны и сохранённые координаты остаются в исходном пиксельном размере Lineage II.

После выбора всех шести областей нажми «Проверить снимки». Они обрабатываются по порядку: сначала кадр с видимым заголовком запоминает магазин, затем кадр с tooltip добавляет предмет. Подключать игровое окно для этого не нужно.

Над подробными результатами выводятся уникальные строки вида Покупка · Lui · Animal Skin · 18 шт. · 400 Adena. Количество читается из числовых скобок в конце названия, например (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
S
Description
No description provided
Readme
11 MiB
Languages
JavaScript 79.5%
CSS 14.2%
HTML 6.3%