• Новые темы в этом разделе публикуются автоматически при добавлении файла в менеджер ресурсов.
    Ручное создание новых тем невозможно.

Мануал Геодата и координатная сетка. Основы.

Aristo

Легендарный
Легенда Истоков
Победитель в номинации 2024
Победитель в номинации 2023
Сообщения
853
Розыгрыши
0
Решения
11
Репутация
739
Реакции
1 042
Баллы
1 733
Всем привет.
Хочу поделится информацией базовых принципах хранения и обработки геоданных, а также о том, как это связано с координатной сеткой.
Сразу скажу, я пишу этот мануал в разделе PTS сервера, но он одинаково справедлив(кроме описанной структуры хранения геоданных!) для любой сборки.

Общая структура и описание:
Игровой мир в L2 описывается тремя координатами. XYZ.
Ось X это направление Запад-Восток(Уменьшение координаты X - это движение на Запад, следовательно увеличение X - движение на Восток)
Ось Y это направление Север-Юг(Уменьшение координаты Y - это движение на Север, следовательно увеличение Y - движение на Юг)
Ось Z это высота. Из-за особенностей хранения гео-данных на PTS сервере, высота ограничена диапазоном 16 битного значения (От -16384 до 16383)

Координатная сетка очень плотно связана с геодатой. Далее в тексте, если специально не указано иное, то следует считать, что описания даются в плоскости XY.
Игровой мир делится на Регионы. Обычный Регион представляет из себя квадрат, с размерами 32768 x 32768 точек
Регион в свою очередь делится на 65535 Блоков(сетка 256х256). Таким образом, размер одного Блока 128 x 128 точек.
Блоки бывают трех основных типов(Плоский, Комплексный и Мультислойный). В чем их отличие и особенности я объясню ниже.
Комплексные и Мультислойные Блоки делятся еще на более мелкие элементы - Ячейки. Один Блок вмещает в себе 64 Ячейки в виде сетки 8 х 8. Следовательно одна Ячейка представляет из себя квадрат 16 х 16 точек.
Мельчайшим объектом мира L2 является Точка. Каждая Точка имеет координаты XYZ.

Как хранятся и обрабатываются геоданные на PTS сервере:
Для хранения геоданных, PTS-сервер использует бинарные файлы, в формате X_Y_conv.dat, где X и Y это номера Региона.
Т.к бинарные файлы подразумевают хранение данных в виде массива байт, то для получения каких-то данных, в большинстве случаев происходит сбор чисел больших диапазонов из определенного количества байт.
Первые 18 байт каждого файла геоданных содержат так называемый Заголовок:
0 байт - X региона
1 байт - Y региона
2-5 байты остались для меня загадкой, я подозреваю что это два 16 битных значения, но их смысл не понятен, т.к они не изменяются.
6-9 байты образуют 32 битное значение, которое хранит в себе количество Ячеек в регионе.
10-13 байты образуют 32 битное значение, которое хранит в себе количество НЕ Мультислойных блоков в регионе
14-17 байты образуют 32 битное значение, которое хранит в себе количество ПЛОСКИХ блоков в регионе.

Дальше хранится информация о самих блоках региона по порядку от 0 до 65534 позиции.
Для удобства восприятия, я буду считать восемнадцатый байт файла региона как нулевой по индексу.
0 и 1 байты хранят 16 битное значение, которое указывает на тип блока.
0x00 - Это Плоский блок. Размер данных 4 байта.
0x40 - Комплексный блок. Размер данных 128 байт.
ВСЕ остальные это Мультислойный блок(В этом случае, тип также указывает еще и на размер блока: [Тип * 2 + 64 * 2 + 2]

Как и какая информация хранится в Блоке?
Для этого нужно небольшое отступление. В мире L2 за возможность движения в определенном направлении, сквозь Ячейку, отвечает еще одна ее характеристика, условно обозначенная как NSWE. Это битовая маска в диапазоне от 0 до 15 включительно.
Что она означает? Это аббревиатура состоящая из первых букв сторон света: North South West East

0 - Движение через ячейку запрещено.
1 - E (1 << 0) Движение на Восток X+
2 - W (1 << 1) Движение на Запад X-
3 - WE (W | E) X+ || X-
4 - S (1 << 2) Движение на Юг Y+
5 - SE (S | E) X+ && Y+
6 - SW (S | W) X- && Y+
7 - SWE (S | W | E) (X- || X+) && Y+
8 - N (1 << 3) Движение на Север Y-
9 - NE (N | E) X+ && Y-
10 - NW (N | W) X- && Y-
11 - NWE (N | W | E) (X- || X+) && Y-
12 - NS (N | S) Y- || Y+
13 - NSE (N | S | E) (Y- || Y+) && X+
14 - NSW (N | S | W) (Y- || Y+) && X-
15 - NSWE (N | S | W | E) X- || X+ || Y- || Y+

Т.е условно, если у вас персонаж стоит в координате 12345, 6789, а вы кликаете в координату 12345, 9876 (т.е Y+) то пройти вы сможете только через те Ячейки, NSWE значение которых удовлетворит выражению:
(NSWE & S) == S


Вернемся к блоку. Что в нем хранится? Каждый тип Блока хранит данные по своему. Разберем их поочереди.
Плоский Блок - Т.к этот тип блока не делится на Ячейки, то значение проходимости общее для всех координат блока(и на PTS это всегда 15, т.е Плоский блок всегда проходим. Плоский блок НЕ ХРАНИТ NSWE). Мы знаем, что размер Плоского блока - 4 байта. Это два 16 битных значения. Каждое значение содержит в себе высоту этого блока в формате ( byte1 << 8 | byte0 & 0xff) & 0x0fff0)
Комплексный Блок -
Т.к этот блок разделен на 64 ячейки, а общий размер блока 128 байт, то мы легко вычисляем, что каждая ячейка блока содержит два байта, которые в свою очередь собираются в 16 битное значение.
Как из одного значения получить NSWE и высоту? Для этого нужно использовать следующую формулу:
Высота - ( ( byte1 << 8 | byte0 & 0xff) & 0x0fff0) >> 1 (т.е фактически деление пополам исходного значения)
NSWE - ( ( byte1 << 8 | byte0 & 0xff) & 0x0F) (т.е фактически мы берем младшие разряды числа)
Мультислойный Блок - Представляет из себя по сути Комплексный блок, но каждая ячейка которого может содержать как одно, так и несколько значений-слоев(в каждом значении будет зашифрована собственная высота и NSWE)

Как соотнести мировые координаты и конкретный блок в мире L2:

Центр мира L2 с координатами 0, 0 ВСЕГДА находится в квадрате 20_18. Следовательно этот регион мы можем обозначить как Нулевой Регион.
ZERO_R_X = 20;
ZERO_R_Y = 18;
Из загруженных гео-данных, мы можем узнать минимальные и максимальные индексы Регионов. (R_X_MIN, R_Y_MIN, R_X_MAX, R_Y_MAX)
Как мы помним - размер региона по каждой стороне 32768

Имея эти данные, мы можем расчитать координаты противолежащих углов Карты Мира по формулам:
WORLD_MAP_MIN_X = (R_X_MIN - ZERO_R_X) * 32768
WORLD_MAP_MIN_Y = (R_Y_MIN - ZERO_R_Y) * 32768

WORLD_MAP_MAX_X = ((R_X_MAX - ZERO_R_X) + 1) * 32768
WORLD_MAP_MAX_Y = ((R_Y_MAX - ZERO_R_Y) + 1) * 32768

Вот расчет для High-Five карты мира:
WORLD_MAP_MIN_X = (11 - 20) * 32768 = -294912
WORLD_MAP_MIN_Y = (10 - 18) * 32768 = -262144
WORLD_MAP_MAX_X = ((26 - 20) + 1) * 32768 = 229376
WORLD_MAP_MAX_Y = ((26 - 18) + 1) * 32768 = 294912

Т.е карта ХФ представляет из себя прямоугольник с вершинами в точках (-294912, -262144) и (229376, 294912)

Теперь самое важное: Как получить блок(или даже ячейку) из координат?
Вот пошаговые действия для координат XY:

Шаг 1) Восстановим координаты региона из мировых
REGION_X = (x - WORLD_MAP_MIN_X >> 4) >> 11
REGION_Y = (y - WORLD_MAP_MIN_Y >> 4) >> 11

Шаг 2) Определим позицию блока по X и Y внутри региона
BLOCK_X = ((x - WORLD_MAP_MIN_X >> 4) >> 3) % 256
BLOCK_Y = ((y - WORLD_MAP_MIN_Y >> 4) >> 3) % 256

Шаг 3) Если искомый блок не плоский, то мы можем получить так же и ячейку.
CELL_INDEX = (((x - WORLD_MAP_MIN_X >> 4) % 8) << 3) + (((y - WORLD_MAP_MIN_Y >> 4) % 8) % 8)


Я не педагог и не писатель. Заранее прошу прощения за сумбур в изложении.
 
Последнее редактирование:
Про проект спасибо) Сурсы изучил. Мне ее не только конвертитьвать надо но и подгружать в свой сервер :pandaredlol:
плохо изучали.
В сурсах написано как считывать каждый формат гео-данных и хранить в едином классе.
 

плохо изучали.
В сурсах написано как считывать каждый формат гео-данных и хранить в едином классе.
Затык был в том, что оригинальный файл .dat парсится нормально
А сконвертированный l2j > conv нет.

Да, мультиблоки можно посчитать но мне их нужно было пропустить для теста. Поэтому я опирался на ОП код он же тип (тип + 64) * 2 и у меня получилось построить карту. Но к конвертированым файлам это не подходит.

Если просчитывать мультиблок и считать ячейки то результат наверное будет однинаковый для всех .dat но мне их надо было пропускать.
 
в conv dat две высоты для flat блоков
в l2d отдельно записывается и просчитывается nswe
та и все
А зачем нужно знать две высоты? Они как-то различаются? Или это по типу зон где описывается высота?
 
А зачем нужно знать две высоты? Они как-то различаются? Или это по типу зон где описывается высота?
FLAT это вольная интепретация COMPLEX блока, с разницей в высоте не более Н юнитов.
То-есть одна высота - это высшая точка COMPLEX блока,
А вторая высота - это низшная точка COMPLEX блока
 
FLAT это вольная интепретация COMPLEX блока, с разницей в высоте не более Н юнитов.
То-есть одна высота - это высшая точка COMPLEX блока,
А вторая высота - это низшная точка COMPLEX блока
Понятно что при COMPLEX блокe такая информация нужнa, а зачем ее пихают в FLAT блок? Притом допустим что даже при блоке COMPLEX она может использоваться, но как и зачем? Высотная информация будет более полезнее в многослойном блоке. Но мне интересно как такая информация используется на сервере L2OFF. Просто както непонятно зачем ее корейцы сохраняли.
 
Понятно что при COMPLEX блокe такая информация нужнa, а зачем ее пихают в FLAT блок? Притом допустим что даже при блоке COMPLEX она может использоваться, но как и зачем? Высотная информация будет более полезнее в многослойном блоке. Но мне интересно как такая информация используется на сервере L2OFF. Просто както непонятно зачем ее корейцы сохраняли.
flat блок не требует расчёт NSWE для прохода сквозь него
Он сам по себе занимает 4 байта, когда COMPLEX - 8 * 8 * 2
Не нужно проверять по ячейкам, ибо у тебя одна ячейка по сути
 
flat блок не требует расчёт NSWE для прохода сквозь него
Он сам по себе занимает 4 байта, когда COMPLEX - 8 * 8 * 2
Не нужно проверять по ячейкам, ибо у тебя одна ячейка по сути
Вопрос ведь не про то идет расчет или нет. А про высотную информацию записанную на этих блоках. Ведь даже при расчетe она не нужна, так как при плоском блоке, там одна величина на Z, на весь блок. А при COMPLEX будут полные значения высот и прохождения NSWE в два байта на ячейку из поля 8х8 . Но я к чему? Зачем нам нужны высоти по максимуму и минимуму блока?
 
Понятно что при COMPLEX блокe такая информация нужнa, а зачем ее пихают в FLAT блок? Притом допустим что даже при блоке COMPLEX она может использоваться, но как и зачем? Высотная информация будет более полезнее в многослойном блоке. Но мне интересно как такая информация используется на сервере L2OFF. Просто както непонятно зачем ее корейцы сохраняли.
Переход с верхней границы одного плоского блока может быть не возможен по высоте на верхнюю границу соседнего плоского блока, но может быть возможен на нижнюю. В этом случае проход возможен, т.к у таких блоков NSWE == 15 всегда. Разница высот плоского блока(на самом деле он называется гео-сектор) не более 32.
 
Переход с верхней границы одного плоского блока может быть не возможен по высоте на верхнюю границу соседнего плоского блока, но может быть возможен на нижнюю. В этом случае проход возможен, т.к у таких блоков NSWE == 15 всегда.
Погоди, если оба блока плоские, то в чем проблема перехода? Если оба блока имеют проход по NSWE = 15 зачем сравнивать высоты?
 
Погоди, если оба блока плоские, то в чем проблема перехода? Если оба блока имеют проход по NSWE = 15 зачем сравнивать высоты?
При движении вниз проблем нет, но при движении вверх геодвиг посчитает такой переход за препятствие.
 
При движении вниз проблем нет, но при движении вверх геодвиг посчитает такой переход за препятствие.
Вот, согласен! Даже движек должен будет понимать что высота перехода с ячейки на ячейку будет выше предельной высоты перехода (ну например в рост персонажа, там 60 и больше единиц).
 
Вот, согласен! Даже движек должен будет понимать что высота перехода с ячейки на ячейку будет выше предельной высоты перехода (ну например в рост персонажа, там 60 и больше единиц).
26


В ПТС геодвиг не работает с блоками. Он работает с ячейками. То что лыжа называет блоки, в ПТС называется сектор и носит абстрактный характер. Т.е у него функций просто удобно сгруппировать ячейки для хранения в бинаре. После загрузки в память, сектор существует просто как индекс, но в случае с плоским блоком, у него есть две базовых ячейки, которые он статично отдает на запрос по любому из внутренних индексов(всегда верхнюю), а нижняя нужна только для проверки проходимости по лимиту высоты.
 
Насчет сравнений. Зачем использовать махимальную высоту плоского блока если есть уже готовое значени высоты по блоку (сама высота блока, не махимум или минимум, ну либо минимум если там только два таких значения) ?
 
Насчет сравнений. Зачем использовать махимальную высоту плоского блока если есть уже готовое значени высоты по блоку (сама высота блока, не махимум или минимум, ну либо минимум если там только два таких значения) ?
Плоский блок, это оптимизированный на уровне билдера комплексный блок, все ячейки которого находятся в рамках дельты высоты между верхней и нижней ячейками. Это сделано опять же для оптимизации. Т.е условно если билдер билдит комплексный блок на 64 ячейки, из которых 63 ячейки имеют высоту 3500, а 1 ячейка имеет высоту 3514, то билдер выгрузит не 128 байт, а 4, сохранив просто высоты 3514 и 3500 как границы блока, т.к 32 это предельно возможный шаг клиента без застревания. А если учесть, что плоских блоков в ХФ около 8.000.000 из 12.000.000, а значений short всего 65535, то логично догадаться, что они кешируются и дедуплицируются. По факту, там около 7000 уникальных значений.
 
Плоский блок, это оптимизированный на уровне билдера комплексный блок, все ячейки которого находятся в рамках дельты высоты между верхней и нижней ячейками. Это сделано опять же для оптимизации. Т.е условно если билдер билдит комплексный блок на 64 ячейки, из которых 63 ячейки имеют высоту 3500, а 1 ячейка имеет высоту 3514, то билдер выгрузит не 128 байт, а 4, сохранив просто высоты 3514 и 3500 как границы блока, т.к 32 это предельно возможный шаг клиента без застревания.
Я понимаю что вы про оптимизацию в размере байтов (ну и места на диске и в рабочей памяти процесса). Но впроc то о другом. Зачем и как сравниваются минимальное и максимальное значения плоского блока.

Кстати, в своем проекте я использую подобные оптимизации что-бы данные занимали меньше байтов в размере. Добился где-то около 35% по урезке всего размера данных. Ну потому что многослойный блок уж очень не оптимизирован на чтение (читается все с начала блока, и идет сравнение каждой ячейки слоя по заданной Z).
 
Вопрос про то как эти значения, махимум и минимум будут использоваться на L2OFF серверe.
Чар стоит на ячейке комплексного блока с высотой 3498. Мувер пытается подвинуть его на следующую клетку. Вызывается функция поиска ближайшей клетки на новых XY. Там внезапно плоский блок с высотой 3532.
1) Кривая реализация геодвига, использующая только 1 высоту: Чар сосет бибу, т.к переход с 3498 на 3532 невозможен, при шаге в 32.
2) Нормальная реализация, где у плоского блока две высоты: 3532-3500. Функция обрабатывает плоский блок аналогично мультислойному, и не найдя перехода на верхнюю ячейку, отдает ближайшую к 3498, а именно 3500. Чар делает переход, но т.к высота всегда берется от верхней границы, его перекидывает на 3532 высоту. IMG_4614.webp
Что-то вроде такого
 
Последнее редактирование:
Кстати, в своем проекте я использую подобные оптимизации что-бы данные занимали меньше байтов в размере. Добился где-то около 35% по урезке всего размера данных. Ну потому что многослойный блок уж очень не оптимизирован на чтение (читается все с начала блока, и идет сравнение каждой ячейки слоя по заданной Z).
Довольно тяжело оптимизировать ячейку из 2 байт, при условии, что любой указатель на нее, будь то ссылка или примитив с индексом, будет весить минимум 4))) А я еще видел гениев, которые 450.000.000 ячеек оборачивали в объекты, получая оверхед в 800%.
У меня плоские блоки задедуплицированны и закешированы, комплексные и мультислойные оптимизированы по структуре для быстрого вычисления диапазона индексов ячеек, и хранятся в офхипе, вместе с графом для пасфинда в виде непрерывного байтового массива. Доступ к ячейкам по офсету, который считается исходя из координат.
 
Последнее редактирование:
Довольно тяжело оптимизировать ячейку из 2 байт, при условии, что любой указатель на нее, будь то ссылка или примитив с индексом, будет весить минимум 4)))
Оптимизируется весь блок. Смотря нa Яву и то как люди до сих пор используют все тот же формат данных, просто не понимаю как не сделать наоборот. Кстати зачем ссылка, и указатель? Можно просто читать значения через массив данных из памяти. Все это то чтo будет загруженно в память.

Пример. Есть блок COMPLEX, 64 значения по два байта. Вопрос, что можно оптимизировать? Ответ, смотрим на расброс значений и на расброс NSWE переходов. Так вот, оказывается что если разброс будет меньше скажем значения в 254 и NSWE = 15 (что в огромном большенстве случаев), можно использовать не два байта, а один. То есть, два байта идет для определения базовой высоты блока, и 64 идет на разницу высот. 128 - 66 = 62 байта в выигрыш. Почти на 50% меньше.

Это один из примеров оптимизации. Но смысл в том что ведь не байты оптимизируются (для этого можно например все закинуть в Roaring32 и будет примерно такое-же снижение по байтам), а сами блоки, как и многослойные (там идет сортировка по слоям, и опять-таки оптимизация слоя как блока) так и блоки с разбросом по Z либо присутствие значений NSWE != 15.
 
Назад
Сверху