Jump to content
iliya77

Мой блог по говнокоду и всего понемногу

Recommended Posts

image.thumb.png.9800ab4883fd638128c182e6498416cf.png

На днях решил покопаться в l2j говнокоде, раз уж делать совсем нечего. Вытащил l2hardcore, который с 2024 года не трогал. Каким-то неведомым образом, я обнаружил себя рефакторящим приват-сторы и трейд-листы (и так всегда, хотя делать собирался совсем другое :D). 
Внутри код наподобие:

Спойлер

public synchronized void confirm()

public synchronized boolean privateStoreSell(Player player, ItemRequestDto[] itemRequests)
 

 

Который, после выноса логики из модели данных, конечно же, начал принимать на вход TradeList. Synchronized, же, начал лочить уже не объект модели, а синглтон сервиса. Да и вообще, synchronized я не видел уже тыщу лет и раньше лучшим выбором было использовать ReentrantLock, так как он работал шустрее стандартных мониторов Java. А если еще вспомнить про Loom в Java 21+, где мониторы не поддерживаются, то выбор, казалось бы очевиден: просто снести к херам synchronized и заменить на ReentrantLock, который будет валяться в модели.
Единственное, что меня основило от такого решения, лишь то что, в CopyOnWriteArrayList я обнаружил использование монитора, хотя раньше там был ReentrantLock, причем с комментарием: "We have a mild preference for builtin monitors over ReentrantLock when either will do". Интересно, да? Вот и мне так показалось.
В общем, после гугления оказалось вот что:
1. Мониторы заоптимизировали просто в усмерть - лок скиппинг, лок мерджинг и прочие приколы, и даже была таска чтобы переделать блокировки с ReentrantLock на synchronized
2. Лок монитора по Object lock = new Object() - значительно дешевле по памяти, чем создание и поддержание ReentrantLock
3. Мониторы до сих пор привязываются к карьер тредам (Loom) :(

Теперь актуальный юсейдж synchronized выглядит так:
1. Дохрена объектов для синхронизации или важна память
2. Используется только примитивная блокировка
3. Маленькое время блокировки (не IO операции), либо не использовать с Loom
Для всего остального - ReentrantLock.

Share this post


Link to post

Какой игра должна была быть изначально? Никогда не задумывались над этим вопросом? К счастью, если хорошо исследовать, то можно обнаружить эти следы во всеми нами любимом корейском коде:)

В процессе избавления от всеми любимых getInstance синглтонов, то и дело приходится отвлекаться на всякие сторонние вещи, такие, как исправления принципов единой ответственности, да и вообще, архитектурного разделения обязанностей слоев. 
Логика отъезжает в сервисы, данные остаются в данных или разбиваются на несколько DTO; появляются наметки слоя валидации данных и команд, которые будут затем выведены в полноценное API модулей. В эту же кучу уходят и сетевые пакеты, которые также разрезаются на сериализаторы/десериализаторы и слой DTO-данных. 

А мы в l2j. Что это значит? А это значит, что часть пакетов состоит из unknown-параметров или магических констант, которые были взяты когда-то из сниффера трафика. Приходится открывать IDA, NetPro (благослови бог savormix <3) и разбираться с этим ужасом.
Как раз в процессе копания в этой всей радости, я обнаружил, что когда-то, в очень стародавние времена, планировалась ручная раскидка атрибутов персонажа: STR, DEX, CON, INT, WIT, MEN. Прям, как в нормальных RPG-играх!
Следы этого остались в древних пакетах NewCharacterSuccess и RequestCharacterCreate.

Первый пакет говорил клиенту, какой диапазон каждого атрибута может быть выбран для каждого начального класса. Еще и с рекомендуемыми значениями!

    private final List<CharacterTemplate> templates;

    public static class CharacterTemplate {
        private final int race;
        private final int classId;
        private final Attribute strAttr;
        private final Attribute dexAttr;
        private final Attribute conAttr;
        private final Attribute intAttr;
        private final Attribute witAttr;
        private final Attribute menAttr;
    }

    public static /*value*/ class Attribute {
        private final int max;
        private final int base;
        private final int min;
    }

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

    name = readS();
    race = readD();
    sex = readD();
    classId = readD();
    intAttr = readD();
    strAttr = readD();
    conAttr = readD();
    menAttr = readD();
    dexAttr = readD();
    witAttr = readD();
    hairStyle = readD();
    hairColor = readD();
    face = readD();

Такие дела.

Чисто теоретически, можно переделать UI создания персонажа и вернуть раскидывание первичных атрибутов персонажа. Пакетка вполне себе это позволяет, движок клиента под капотом эти данные принимает и крутит.

Share this post


Link to post

image.thumb.png.3e229c9b7c0d10f6018542427e37c257.png

 

Обнаружил забавный баг в IDEA-плагине Manifold. 

При использовании интерполяции строк, если сослаться внутри шаблона на метод, класс которого не используется больше, то IDEA подсвечивает импорт этого класса, как неиспользуемый и весело грохает его при оптимизации импортов.
Уже отписал Скотту по проблеме:)

А для тех, кто не хочет указывать FQN класса в шаблоне строки, пока баг не исправен, есть костыльное решение: просто создать приватный статичный класс с неинициированным полем импортируемого класса.

Share this post


Link to post

Вывод логов клиента: DLL + src ( terminal.zip )
Достаточно просто положить в DalamWorld. Если файл уже существует, то его можно заменить, т.к. orc.dll в новых обновах DW не используется

 

 

 

Share this post


Link to post

Вы когда-нибудь сталкивались с критами? Хе-хе, ну мне то не врите, конечно сталкивались. 
Сегодня я принес поесть небольшую помощь в казуальной отладке клиента игры.

Немного предыстории.
После очередного куска рефакторинга сервера, нужно было протестировать работу этого всего. И вот, исправляю я ошибки и натыкаюсь на прелестный баг: после создания тестового персонажа и возврата в лобби - игра вылетает с критом:

History: IsLoadedResource <- User::SetPawnResource <- NCPawnSelectWnd::AddPawn <- NConsoleWnd::AddCharacterInfo <- d <- CharacterSelectionInfoPacket <- UNetworkHandler::Tick <- Function Name=CharacterSelect <- UGameEngine::Tick <- UpdateWorld <- MainLoop

Понятно, что упал он на загрузке каких-то ресурсов, но не понятно каких именно.


Пару слов о клиенте. 
Максимально чистый, прямиком из корейского 2007 года: полностью снята и очищена Themida со всех файлов; пропатчено выключение GG. Максимально из отличий - полностью пересобранные все dat-файлы, но внутри они оригинальные (кроме system-messages, где добавлена пара новых сообщений). О, ну и да, конечно же пропатчены глифы для поддержки русского языка.


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

Начать я решил с клиента. 
Первым шагом я попытался включить логи через флаг запуска -log - но нихрена. Если он и работает где-то, то точно не на той версии клиента, что у меня. Помучав пару минут нейросеть, она предложила мне добавить в l2.ini такую хрень:

[Core.System]
PurgeLogs=False
SaveLog=True
Log=L2.log
...

Первый раз такое вижу, но попытка - не пытка... Как оказалось: очень даже пытка - включить эти логи. Если оно не хочет работать, значит ему надо помочь и самому включить логи. Во всяком случае, писать на C++ чуть меньшая пытка:) 
Вооружившись IDA, CLion и исходниками UE 2.5 - была быстро написана dll-ка на крестах, которая, тупо, сплайсит вызовы FOutputDevice:Log* и сыпет это в консоль.

В общем, проблема оказалась в кривом пакете CharSelectInfo. При рефакторинге слетело количество элементов в paperdoll-персонажа, из-за чего структура пакета съехала и в face & hair персонажа приходили очень интересные значения, а клиент радостно пытался их жевать, пока не падал:)


А dll-ку с исходниками приложил чуть выше, может кому-нибудь пригодится.

Share this post


Link to post

image.thumb.png.5f2fa07abf0e2299f70b9dfe5aac6ab8.png

Увидел тут забавный код в jctools библиотеке:

    byte b000,b001,b002,b003,b004,b005,b006,b007;//  8b
    byte b010,b011,b012,b013,b014,b015,b016,b017;//  16b
    byte b020,b021,b022,b023,b024,b025,b026,b027;//  24b
    byte b030,b031,b032,b033,b034,b035,b036,b037;//  32b
    byte b040,b041,b042,b043,b044,b045,b046,b047;//  40b
    byte b050,b051,b052,b053,b054,b055,b056,b057;//  48b
    byte b060,b061,b062,b063,b064,b065,b066,b067;//  56b
    byte b070,b071,b072,b073,b074,b075,b076,b077;//  64b
    byte b100,b101,b102,b103,b104,b105,b106,b107;//  72b
    byte b110,b111,b112,b113,b114,b115,b116,b117;//  80b
    byte b120,b121,b122,b123,b124,b125,b126,b127;//  88b
    byte b130,b131,b132,b133,b134,b135,b136,b137;//  96b
    byte b140,b141,b142,b143,b144,b145,b146,b147;//  104b
    byte b150,b151,b152,b153,b154,b155,b156,b157;//  112b
    byte b160,b161,b162,b163,b164,b165,b166,b167;//  120b
    byte b170,b171,b172,b173,b174,b175,b176,b177;//  128b

Сразу стало понятно, что это борьба с false sharing.

 

Цитата

Это когда ядро читает страницу памяти с данными в кеш CPU, а потом хочет ее записать, из-за чего вступает в конкуренцию с другим ядром, которое делало тоже самое, но работало с другими данными, а страница памяти у них одна и та же.

 

Вспомнилось, что в Java когда-то даже ввели аннотацию для автоматического выравнивания памяти - @Contended. Самое забавное, что ее потом тупо выключили для кода, который находится вне бут-лоадера. Функция есть, а воспользоваться нельзя. Почему? Потому-что иди нахрен, вот почему. 😞
Вот и приходится выкручиваться самыми разными способами. Тут разработчики решили вручную забить байты между данными для продюсеров и данными для консюмеров очереди.

Поражает в этом во всем только глубокое отрицалово комитета самой Java, где у них не существует false sharing, memory padding и других вещей, которые лежат чуть-чуть ниже уровня самого языка. При этом, в бесконечной попытке ускорить JVM и стандартную библиотеку, разработчики языка понимают все это и вводят механизмы для работы с данными проблемами, но нет, не дают это пользователям языка, ато вдруг везде понатыкают, верно? Зато, бл$!ь, никому не нужная CORBA у них была и они ее поддерживали десяток-другой лет. Печально 😕

Share this post


Link to post

image.thumb.png.32297bd9674cbdd35fa8cd81125adf8c.png

 

Решил я тут посмотреть очень древние версии L2J (когда они еще не переехали на свой собственный svn/trac). По таймлайну это где-то середина нулевых годов.
Какого же было мое удивление, что довольно большую часть систем, которые еще используются почти во всех l2j-форках - спроектировал и написал один человек. Да-да, в L2J когда-то был архитектор - maximus / mkizub и он же Wooden. 
Причем, судя по тому, что L2J хостился на ныне умершем opensvn - он скорее всего, еще и лидил это все дело. В любом случае, на opensvn оно могло попасть только через Кизуба, т.к. сервис был российским и на русском языке.
Я, конечно, и раньше видел его авторство во многих классах, но не мог связать, что это все один человек, а тут, внезапно, связалось. 

 

Что же заложил в свое время Кизуб?
1. Базу скилл движка. И я говорю не про обработчики скиллов, а про xml-представление, которое почти 1-в-1 сохранилось по сей день: таблицы, вложенности и прочее. Более того, сюда так же входит все что идет от DocumentBase.
2. Первая версия mmocore (еще до выделения в отдельную библиотеку KenM'ом). До этих изменений ребята дрочили байтики напрямую и использовали блокирующие сокеты. На то время, переход на NIO - был прям прорывом на острие ножа технологий Java.
3. Многопоточка. Пулы потоков и базовая модель исполнения игровой логики из очереди обработчиков пакетов. 
4. ... и много другого 🙂

Вот они, настоящие fallen heros 🙂

Хотелось бы увидеть, конечно, как бы выглядел L2J сервер и дальше, если бы он продолжил над ним работать, а не переключился на другие свои проекты. Но имеем, то что имеем.

Share this post


Link to post

Ура-ура. Наконец Valhalla выводят хотя бы в инкубатор. Я 16 лет ждал Project Valhalla и наконец они дотащат это:)
 

Цитата

В кратце - проект Valhalla дает возможность создавать структуры а-ля C#. Если структура является частью класса, тогда она просто инлайнится в этот класс, а если она используется в методе (передается, например), тогда она кладется на стек и копируется, как и примитивные типы в Java.
 

Очень жаль, что в 2027 году только, так как уже есть необходимость value-классов, хотя бы, в той же разработке l2j: инлайн структур в класс. А пока приходится ограничиваться record-классами, как точками, которые будут потом заменены на value. 
В конце концов: не загружать же отдельные сборки JVM с поддержкой value-классов, верно? Тем более, что работать они могут, кхм, несколько нестабильно:)

Share this post


Link to post

Если кому то нужно. Вытащил формулы полета стрелы и эффектов из Dalam World. 

image.png.9f6373649d490c95bea505d99d046ee9.png

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×