/tg/ Station 13 - Modules - Types

Руководство по модуляризации — стиль Nemesis

Несоблюдение данного руководства приведёт к отклонению вашего PR.

Введение

Разработка и поддержка отдельной кодовой базы — большая задача, на которой многие споткнулись и поплатились последствиями: устаревшим и беспорядочным кодом. Это не обязательно вина недостатка квалификации тех, кто её поддерживает — просто такое начинание требует огромных ресурсов и непрерывных усилий.

Одно из решений — построить наш сервер на основе надёжной кодовой базы, которая в первую очередь поддерживается кем-то ещё (в нашем случае это tgstation), и добавлять собственный контент модульно, при этом следуя общим направлениям разработки кода (но не геймплея) апстрима, отзеркаливая их изменения для сохранения паритета.

Git как система контроля версий очень полезен, однако это всё же сугубо методичный инструмент, следующий своим многочисленным алгоритмам, которые, к сожалению, не всегда могут разрешить определённые изменения в коде однозначным образом — что выливается в конфликты, которые нужно разрешать вручную.

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

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

Важное замечание — ТЕСТИРУЙТЕ СВОИ PR

Вы несёте ответственность за тестирование своего контента. Не помечайте pull request как готовый к ревью, пока действительно его не протестировали. Если для тестирования нужен отдельный клиент, можно использовать гостевую учётную запись, выйдя из BYOND и подключившись к своему тестовому серверу. Test merge — это не средство для поиска багов, а инструмент стресс-тестов в ситуациях, когда локальное тестирование попросту не позволяет их провести.

Природа конфликтов

Например, пусть в оригинальном коде есть

var/something = 1

в основной кодовой базе, и мы решаем изменить значение с 1 на 2 на нашей стороне:

- var/something = 1
+ var/something = 2 // NEMESIS EDIT CHANGE - ORIGINAL: var/something = 1

но затем апстрим вносит изменение в свою кодовую базу, меняя значение с 1 на 4:

- var/something = 1
+ var/something = 4

Каким бы простым ни был этот пример, он приводит к относительно простому конфликту вида:

var/something = 2 // NEMESIS EDIT CHANGE - ORIGINAL: var/something = 4

где мы вручную выбираем предпочтительный вариант.

Решение

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

tl;dr — git старается изо всех сил, но в конечном счёте это просто тупая программа, поэтому мы сами должны проделать работу, чтобы он смог выполнить большую часть дела, минимизировав ручные усилия в случаях, когда конфликты всё же неизбежны.

Наш ответ на это — модуляризация кода.

Модуляризация означает, что большая часть наших изменений и дополнений будет храниться в отдельной папке modular_nemesis/, максимально независимой от основного кода, а те изменения, которые невозможно модуляризировать, должны быть корректно помечены комментариями — с указанием, где начинается изменение, где заканчивается и к какой фиче оно относится. Об этом подробнее в следующем разделе.

Регламент модуляризации

Всегда начинайте с того, чтобы продумать тему/назначение своей работы. Часто стоит проверить, нет ли уже существующего модуля, к которому следует добавить ваш функционал.

Если это специфичная для tg-кода правка или багфикс, первое, что нужно сделать — попытаться обсудить это и оформить PR в апстрим, а не плодить лишнюю модуляризацию здесь.

Иначе выберите новый ID для своего модуля. Например, DNA-FEATURE-WINGS, XENOARCHEAOLOGY или SHUTTLE_TOGGLE — мы будем использовать его в дальнейшей документации. По сути, это идентификатор вашего модуля. Он должен быть единым по всему модулю. ВСЕ упоминания должны быть совершенно одинаковыми. Это нужно для удобства поиска.

Затем заведите основную папку, в которой вы будете работать — обычно её имя совпадает с ID модуля. Например, modular_nemesis/modules/shuttle_toggle.

Карты

ВАЖНО: ПРАВИЛА КОНТРИБЬЮЦИИ КАРТ БЫЛИ ОБНОВЛЕНЫ

При добавлении нового объекта на карту вы ОБЯЗАНЫ следовать этой процедуре: Сначала определитесь с масштабом изменения. Если это небольшое изменение с одним объектом — используйте simple area automapper. Если речь о целой комнате — используйте template automapper.

У нас больше не будет map-версий с суффиксом _nemesis.

НЕ МЕНЯЙТЕ ОРИГИНАЛЬНЫЕ TG-КАРТЫ — они держатся под тем же стандартом, что и иконки. Используйте указанные выше инструменты для правки карт.

Automapper использует заранее подготовленные шаблоны, чтобы переопределять участки карты, используя координаты для определения стартовой точки. Примеры см. в записях automapper_config.toml.

Simple area automapper использует datum-записи, чтобы разместить один предмет в области карты, где это имеет смысл в общих чертах.

Ассеты: изображения, звуки, иконки и бинарные файлы

Git плохо разрешает конфликты бинарных файлов, поэтому изменения основных бинарных файлов категорически запрещены, если только у вас нет очень очень очень веской причины поступить иначе.

Все добавляемые нами ассеты должны помещаться в ту же модульную папку, что и ваш код. Это означает, что всё хранится внутри папки вашего модуля — звуки, иконки и файлы кода.

  • Пример: Вы добавляете нового моба для lavaland.

    Сначала создаёте свою модульную папку. Например, modular_nemesis/modules/lavalandmob.

    Затем создаёте подпапки для каждого компонента. Например, /code для кода, /sounds для звуковых файлов и /icons для иконок.

    После этого устанавливаете ссылки внутри кода.

      /mob/lavaland/newmob
        icon = 'modular_nemesis/modules/lavalandmob/icons/mob.dmi'
        icon_state = "dead_1"
        sound = 'modular_nemesis/modules/lavalandmob/sounds/boom.ogg'
    

    Это гарантирует, что ваш код полностью модульный, и упрощает дальнейшие правки.

  • Прочие ассеты, бинарные файлы и инструменты обычно обрабатываются аналогично, в зависимости от конкретного случая. Если сомневаетесь — спросите совета у мейнтейнера или других контрибьюторов.

  • Любые дополнительные файлы иконок одежды, которые вы добавляете, ОБЯЗАНЫ идти в существующие файлы из секции одежды в master_files.

Папка master_files

Любые модульные переопределения иконок, звуков, кода и т. п. вы всегда должны размещать внутри этой папки, и она обязана повторять структуру основной папки кода.

Пример: code/modules/mob/living/living.dm -> modular_nemesis/master_files/code/modules/mob/living/living.dm

Это нужно, чтобы было проще понять, что именно изменилось в базовом файле, не прочёсывая определения процедур.

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

Полностью модульные части вашего кода

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

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

Если коротко: основная часть модульного кода будет лежать в подпапках основной папки вашего модуля — modular_nemesis/modules/yourmodule/code/, по тем же правилам, что и для ассетов. Не повторяйте структуру папок основного кода внутри своей модульной папки.

Например, modular_nemesis/modules/xenoarcheaology/code содержит весь код, инструменты, предметы и механизмы, относящиеся к данному модулю.

Такие модули, если только они не очень просты, обязаны иметь файл readme.md в своей папке, содержащий следующее:

  • ссылки на PR, которые реализовали этот модуль или внесли в него существенные изменения
  • краткое описание модуля
  • список изменённых файлов в основном коде с кратким описанием изменений, а также список изменений в других модульных файлах, не входящих в этот модуль, но необходимых для его корректной работы
  • (опционально) более развёрнутая документация на будущее, которая пригодится при дальнейшей разработке и сопровождении
  • авторы (credits)

Шаблон: Здесь

Модульные override'ы (Важно!!)

Обратите внимание, что можно дописывать код до или после основной процедуры модульно, не редактируя оригинальную процедуру — через обращение к родительской процедуре с помощью . = ..() или ..(). Аналогично, можно добавить новую переменную в существующий datum или obj, не редактируя основные файлы.

Замечание про override процедур: если вы можете — это ещё не значит, что вы должны!!

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

Например: пожалуйста, не копируйте целиком TG-процедуру в модульный override, не меняйте в ней одну мелочь и не объявляйте это «полностью модульным решением». Такие процедуры — абсолютный кошмар для поддержки, потому что как только что-то меняется в апстриме, вам придётся обновлять переопределённую процедуру.

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

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

Также следует учитывать производительность, когда вы переопределяете «горячую» процедуру (например, Life()), поскольку каждый дополнительный вызов добавляет накладные расходы. В таких случаях Nemesis-правки в основном коде значительно производительнее. Впрочем, для большинства процедур об этом думать не придётся.

Эти модульные override'ы должны храниться в master_files, и старайтесь не размещать их внутри модулей.

Чтобы не усложнять, допустим, вы хотите, чтобы оружие искрило при выстреле — для имитации дульной вспышки или по любым другим причинам, — и потенциально хотите, чтобы это работало со всеми видами оружия.

Можно начать в модульном файле с добавления переменной:

/obj/item/gun
    var/muzzle_flash = TRUE

И это просто будет работать. Дальше, допустим, вы хотите проверять эту переменную и порождать искры после выстрела. Зная, что оригинальная процедура, вызываемая при выстреле — это

/obj/item/gun/proc/shoot_live_shot(mob/living/user, pointblank = 0, atom/pbtarget = null, message = 1)

вы можете объявить дочернюю процедуру, которая встроится в цепочку наследования родственных процедур (звучит мудрёно, но в таких простых случаях беспокоиться не о чем):

/obj/item/gun/shoot_live_shot(mob/living/user, pointblank = 0, atom/pbtarget = null, message = 1)
    . = ..() //. — это значение по умолчанию для возврата; мы присваиваем ему то, что вернёт родительская процедура, так как вызываем её до нашего кода
    if(muzzle_flash)
        spawn_sparks(src) //Для простоты предполагаем, что вы уже сделали для этого процедуру

На этом основы можно считать раскрытыми.

Немодульные изменения основного кода — ВАЖНО

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

Обязательно фиксируйте такие изменения в readme.md модуля. Любые изменения файлов.

В таких случаях мы условились применять следующую конвенцию, с примерами:

  • Добавление:

    // NEMESIS EDIT ADDITION START - SHUTTLE_TOGGLE - (Необязательная причина/комментарий)
    var/adminEmergencyNoRecall = FALSE
    var/lastMode = SHUTTLE_IDLE
    var/lastCallTime = 6000
    // NEMESIS EDIT ADDITION END
    
  • Удаление:

    
    /* // NEMESIS EDIT REMOVAL START - SHUTTLE_TOGGLE - (Необязательная причина/комментарий)
    for(var/obj/docking_port/stationary/S in stationary)
      if(S.id = id)
        return S
    */ // NEMESIS EDIT REMOVAL END
    WARNING("couldn't find dock with id: [id]")
    

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

    /* // NEMESIS EDIT REMOVAL START - SHUTTLE_TOGGLE - (Moved to modular_nemesis/shuttle_toggle/randomverbs.dm)
    /client/proc/admin_call_shuttle()
    set category = "Admin - Events"
    set name = "Call Shuttle"
    
    if(EMERGENCY_AT_LEAST_DOCKED)
      return
    
    if(!check_rights(R_ADMIN))
      return
    
    var/confirm = alert(src, "You sure?", "Confirm", "Yes", "No")
    if(confirm != "Yes")
      return
    
    SSshuttle.emergency.request()
    SSblackbox.record_feedback("tally", "admin_verb", 1, "Call Shuttle") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
    log_admin("[key_name(usr)] admin-called the emergency shuttle.")
    message_admins(span_adminnotice("[key_name_admin(usr)] admin-called the emergency shuttle."))
    return
    */ //NEMESIS EDIT REMOVAL END
    
  • Изменение:

    if(SHUTTLE_STRANDED, SHUTTLE_ESCAPE, SHUTTLE_DISABLED) // NEMESIS EDIT CHANGE - ORIGINAL: if(SHUTTLE_STRANDED, SHUTTLE_ESCAPE)
    

    Всегда указывайте оригинальный код (всю строку целиком!) и используйте такой же формат, как выше.

    Многострочных изменений СЛЕДУЕТ ИЗБЕГАТЬ. Либо разбейте их на несколько однострочных изменений, либо сделайте так:

    Пример: связка «удаление + добавление». Это предпочтительный способ обработки изменений, занимающих больше одной строки и имеющих разные уровни отступов.

    /* // NEMESIS EDIT REMOVAL START - Adds conditional
    	return 1
    */ // NEMESIS EDIT REMOVAL
    // NEMESIS EDIT ADDITION START - Adds conditional
    	if(!isnull(src))
    		return 1
    // NEMESIS EDIT ADDITION END
    
    Так нам гораздо проще разрешать диффы во время merge-конфликтов, потому что
    диффы становятся очень понятными и наглядными.
    

Исключительные случаи модульного кода

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

Define'ы, helper'ы и globalvar'ы

Из-за того, как BYOND загружает файлы, пришлось завести отдельную папку для обработки наших модульных define'ов. Эта папка — code/__DEFINES/~nemesis_defines, в которой можно добавлять define'ы в существующие файлы или создавать новые файлы по мере необходимости.

Если ваш define используется более чем в одном файле, он обязан быть объявлен здесь.

Если define используется в одном файле и нигде больше не понадобится — объявите его в начале файла и поставьте #undef MY_DEFINE в конце. Это нужно, чтобы контекстные меню оставались чистыми, и чтобы не путать тех, кто пользуется IDE с автодополнением.

Те же правила относятся к helper-функциям и глобальным переменным — их соответствующие папки это code/__HELPERS/~nemesis_helpers и code/_globalvars/~nemesis_globalvars

Структура папки модуля

Чтобы соблюсти форму, обеспечить удобную навигацию по большинству модулей и контролировать количество создаваемых в репозитории файлов и папок — вы обязаны придерживаться следующей структуры.

Названия папок должны быть строго такими, как указано.

Самая верхняя папка: module_id

НЕ КОПИРУЙТЕ СТРУКТУРУ ФАЙЛОВ ОСНОВНОГО КОДА В СВОЙ МОДУЛЬ!!

Code: Все файлы .DM должны лежать здесь.

  • Хорошо: /modular_nemesis/modules/example_module/code/disease_mob.dm
  • Плохо: /modular_nemesis/modules/example_module/code/modules/antagonists/disease/disease_mob.dm

Icons: Все файлы .DMI должны лежать здесь.

  • Хорошо: /modular_nemesis/modules/example_module/icons/mining_righthand.dmi
  • Плохо: /modular_nemesis/modules/example_module/icons/mob/inhands/equipment/mining_righthand.dmi

Sound: Все звуковые файлы должны лежать здесь.

  • Хорошо: см. выше.
  • Плохо: см. выше.

readme.md должен лежать в родительской папке — module_id.

НЕ СМЕШИВАЙТЕ РАЗНЫЕ ТИПЫ ФАЙЛОВ В ОДНИХ ПАПКАХ!

Закомментированный код — НЕ ДЕЛАЙТЕ ЭТОГО

Если вы убираете лишний код в модулях — не комментируйте его, а удаляйте.

Даже если вам кажется, что кто-то потом доработает то, что вы комментируете, — не надо, для этого есть git blame.

То же касается и файлов: не комментируйте целые файлы — просто удаляйте их. Это помогает сокращать раздувание репозитория и бессмысленные комментарии.

Это правило не относится к немодульным изменениям.

Модульный TGUI

TGUI — ещё один исключительный случай, поскольку он использует JavaScript и не может быть модульным тем же способом, что и DM-код. ВСЕ файлы TGUI находятся в /tgui/packages/tgui/interfaces и его подкаталогах; отдельной папки для интерфейсов Nemesis не существует.

Изменение апстримных файлов

При изменении апстримных TGUI-файлов действуют те же правила, что и при правке апстримного DM-кода, однако синтаксис комментариев может слегка отличаться.

Можно использовать как // NEMESIS EDIT, так и /* NEMESIS EDIT */, хотя в некоторых случаях придётся выбрать что-то одно.

В целом старайтесь держать комментарий о правке на той же строке, что и само изменение. Желательно — внутри JSX-тега. Например:

<Button
	onClick={() => act('spin', { high_quality: true })}
	icon="rat" // NEMESIS EDIT ADDITION
</Button>
<Button
	onClick={() => act('spin', { high_quality: true })}
	// NEMESIS EDIT ADDITION START - ещё один пример, многострочные правки
	icon="rat"
	tooltip="spin the rat."
	// NEMESIS EDIT ADDITION END
</Button>
<SomeThing someProp="whatever" /* работает и в самозакрывающихся тегах */ />

Если так сделать нельзя, можно обернуть свою правку в фигурные скобки, например:

{
	/* NEMESIS EDIT ADDITION START */
}
<SomeThing>someProp="whatever"</SomeThing>;
{
	/* NEMESIS EDIT ADDITION END */
}

Создание новых TGUI-файлов

ВАЖНО! При создании нового TGUI-файла с нуля, пожалуйста, добавляйте в самом верху файла (строка 1) следующее:

// THIS IS A NEMESIS UI FILE

Так такие файлы легко опознаются как модульные TGUI .tsx/.jsx. Больше ничего делать не нужно, и в модульном TGUI-файле никогда не понадобится комментарий вида «Nemesis edit».

Послесловие

Может показаться, что это много, но если мы будем последовательны, в долгосрочной перспективе это избавит нас от множества головных болей — когда дело дойдёт до ручного разрешения конфликтов. Благодаря чуть более тщательной документации сразу будет видно, какие изменения были сделаны, где и какими фичами — всё станет гораздо менее запутанным и неоднозначным.

Удачи в коде. Помните: если вам нужна помощь — сообщество всегда рядом.