L2 Market Parser

Кроссплатформенный локальный прототип для захвата нескольких окон Lineage II и сбора предметов из tooltip торгового окна.

Запуск

Нужен Node.js 20 или новее и Chrome либо Edge.

npm install
cp .env.local.example .env.local
# Сгенерируй один случайный токен, сразу сохрани его и в .env.local,
# и как новый Gitea Secret HOME_SERVICE_L2_MARKET_IMPORT_TOKEN.
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 :, поэтому сам tooltip тоже может двигаться при наведении на разные предметы.

Из заголовка 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. Отдельно выдели точную строку названия и точную строку цены. Для названия лучше использовать длинный предмет и оставить запас справа.
  8. Сохрани калибровку, нажми «Начать сбор» и вернись в игру.
  9. Сначала оставь курсор вне предметов, пока в результате не появятся тип сделки и имя торговца.
  10. Наводи курсор на каждый предмет. Даже если tooltip закроет заголовок, предмет будет записан в последний найденный магазин.

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

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

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

Размер игрового окна и положение торгового окна могут отличаться. Масштаб интерфейса 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)

Лучше калиброваться на предмете с длинным названием и большой ценой. Тогда прямоугольники не обрежут более короткие варианты.

Диагностика

В результате показывается последний обработанный кадр с рамками и каждый этап отдельно:

  • зелёная рамка означает, что шаблон прошёл заданный порог;
  • красная показывает лучшее совпадение, которое не прошло порог;
  • синяя показывает вычисленную область следующего шага;
  • «Пропущено» означает, что предыдущий обязательный этап не прошёл;
  • рядом с 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%