Перед вами руководство по внесению вклада в билд Paradise проекта SS1984. Здесь вы найдёте как советы по работе с Github, так и стандарты разработки/написания кода, которые являются обязательными для ознакомления и знания.
- Github
- Спецификации
- Объектно-ориентированный код
- Все пути должны быть полноценными
- Пользовательские интерфейсы (UI)
- Не переопределяйте проверки типов
- Пути объектов должны начинаться с
/ - Пути
datumобъектов должны начинаться с "datum" - Не используйте строковое определение путей
- Используйте
[A.UID()]вместо\ref[A] - Используйте формат
var/nameпри объявлении переменных - Используйте табуляцию, а не пробелы
- Не пишите "костыльный" код
- Не дублируйте код
- Компромиссы при старте и во время выполнения: списки и "скрытая" процедура
init - Приоритет
Initialize()вместоNew()для атомов - Не используйте неявный
var/ - Не используйте "магические" числа и строки
- Управляющие конструкции
- Используйте ранний возврат
- Используйте
addtimer()вместоsleep()илиspawn() - Устаревший код (legacy)
- Пишите безопасный код
- Работа с файлами
- Прочие замечания
- Особенности Dream Maker
- Операторы
- SQL
- Rust
- Стандарты маппинга
Перед вами руководство по внесению вклада в билд Paradise проекта SS1984. Здесь вы найдёте как советы по работе с Github, так и стандарты разработки/написания кода.
Если вы оставляете комментарий под чьим-то Pull Request'ом (далее "PR"), убедитесь, что он ёмко и лаконично выражает вашу мысль. Не следует писать на отвлечённые темы или выражать агрессию/токсичность по отношению к автору — будьте дружелюбны и пишите по теме PR'а.
Вклад в разработку проекта и создание PR'ов игроками приветствуется и поощряется. Учтите, что ваш PR может не получить одобрение от Ведущего Разработчика, либо вас попросят его доработать. Это особенно актуально для PR'ов большого размера или тех, которые затрагивают баланс. Совещайтесь с другими разработчиками и задавайте вопросы. Вы же не хотите потратить своё время и силы на работу, которую в итоге забракуют.
-
PR'ы должны быть атомизированы. Разбивайте значимые изменения на отдельные commit'ы. Благодаря такому подходу, в случае необходимости модификации/удаления определённого изменения вы можете работать только с соответствующим commit'ом. Хотя следование этому правилу не всегда является возможным из-за ограничений движка, старайтесь не пренебрегать им по возможности.
-
Тщательно документируйте и объясняйте свои PR'ы. Это особенно актуально, если вы портируете PR с другой кодбазы (TG station, например), структура кода которой может разительно отличаться от нашей. Чем лучше вы подходите к документированию своих работ, тем легче членам ревью-команды проверять ваши PR'ы и понимать ваш ход принятия решений. Иногда даже небольшая ремарка может значительно ускорить процесс одобрения ваших изменений.
-
PR'ы не должны иметь каких-либо merge-конфликтов. Используйте
git rebaseилиgit reset, чтобы обновлять свои ветки, неgit pull. -
Всегда объясняйте, почему ваш PR должен быть одобрен и какую ценность для проекта несут данные изменения. Этим правилом можно пренебречь, если вы считаете, что польза PR'а очевидна и не требует каких-либо дополнительных пояснений. Тем не менее, дополнительная информация никогда не будет лишней.
Перед заголовком вашего PR'а требуется поставить корректный тег, что необходимо для корректного отображения оного в Changelog-меню игры, а также для сортировки PR'ов на Github. Названия PR'а рекомендуется писать на русском, хотя это не является обязательным (особенно для чисто технических изменений).
Учтите, что вы можете использовать мульти-теги, прописывая теги PR'а через /.
Пример:
bugfix: пропажа спрайта одежды при смене её внешнего вида
add/local: новый босс для Лазиса + локализация старых боссов
Список тегов для PR'а:
- add: Вы добавляете новый контент в игру.
- admin: Вы изменяете что-то, связанное с администрацией (кнопки, управление, панели, щитспавн и т.д.)
- balance: Вы производите балансировку в игре.
- bugfix: Вы исправляете некий баг.
- code_imp: Вы имплементируете новое для билда, не изменяя при этом ничего в самой игре.
- config: Вы изменяете конфиг или работу SQL.
- del: Вы удаляете контент из игры.
- experiment: Ваш PR создан с целью какого-то эксперимента.
- map Вы изменяете только карту.
- local Вы переводите текст на русский.
- imageadd: Вы добавляете новый спрайт.
- imagedel: Вы удаляете старый спрайт.
- soundadd: Вы добавляете новый звук.
- sounddel: Вы удаляете старый звук.
- spellcheck: Вы исправляете опечатку.
- tweak: Вы внесли незначительную правку.
- refactor: Вы полностью переписываете старый код, улучшая его, НО не изменяя функционал.
- server: Вы изменяете что-то связанное с серверной частью или Github.
- wip: Ваш PR в драфте и вы планируете разрабатывать его в течение долгого времени.
Ниже перечислены стандарты написания кода и разработки в общем. Вы обязаны им следовать, чтобы в билд попадал только качественно написанный код, удобный для дальнейшей разработки. Чем лучше ваш PR соответствует этим правилам, тем больше своего и чужого времени вы сэкономите и тем быстрее ваши изменения попадут в билд. Большое спасибо за ознакомление с этим разделом!
Dream Maker, язык движка BYOND, (далее "DM") — объекто-ориентированный язык, поэтому написанный вами код должен соответствовать стандартам ООП по возможности. Если "ООП" вам ни о чём не говорит, крайне рекомендуем вам поискать материалы по данной теме, чтобы иметь хотя бы базовое понимание.
Иными словами, пути должны быть абсолютными.
DM позволит вам поместить почти что любое ключевое слово в блок, например:
datum
datum1
var
varname1 = 1
varname2
static
varname3
varname4
proc
proc1()
code
proc2()
code
datum2
varname1 = 0
proc
proc3()
code
proc2()
..()
codeТем не менее, использование такого подхода запрещено.
Исключение из данного правила: большая часть объектов в файле имеет относительные пути, что делает нахождение определений по полному пути практически невозможным.
Вот исправленная версия кода выше с использованием абсолютных путей:
/datum/datum1
var/varname1
var/varname2
var/static/varname3
var/static/varname4
/datum/datum1/proc/proc1()
code
/datum/datum1/proc/proc2()
code
/datum/datum1/datum2
varname1 = 0
/datum/datum1/datum2/proc/proc3()
code
/datum/datum1/datum2/proc2()
..()
codeВсе новые пользовательские интерфейсы (они же "UI"), добавляемые в игру, должны быть созданы с помощью TGUI фреймворка. Соответствующая документация может быть найдена в папке tgui/docs. Это необходимо для того, чтобы все игровые UI были работоспособными и удобными для использования.
Исключение: если конкретный UI предназначен только для OOC поля (админ. функционал, к примеру), то требованием к TGUI можно пренебречь. Тем не менее, если вы сделаете TGUI, то хуже не будет.
Использовать : оператор, чтобы переопределить проверку на тип объекта запрещено. Вы должны приводить переменную к корректному типу.
То есть: /datum/thing, но не datum/thing.
Хоть это и опционально в DM, но использование именно такого стиля облегчает обнаружение и поиск определений объектов в коде. Для понимания, путь /arbitrary функционально идентичен пути /datum/arbitrary. И всё же, определяйте пути по примеру второго объекта.
Пример:
// Хорошо
var/path_type = /obj/item/baseball_bat
// Плохо
var/path_type = "/obj/item/baseball_bat"Использование строковых путей редко допускается, поскольку в таком случае компилятор не выдаст ошибку, если указанный путь перестанет существовать, что может привести к ошибкам во время выполнения.
В BYOND существует система передачи "мягких ссылок" на датумы (datum) с использованием формата "\ref[datum]". Это позволяет находить объект только по текстовой строке, что особенно удобно при взаимодействии кода BYOND с HTML/JS в пользовательских интерфейсах. При возврате в BYOND ссылка преобразуется обратно в датум с помощью locate("\ref[datum]"). Проблема заключается в том, что если исходный объект был удалён, locate() может вернуть совершенно другой объект — BYOND повторно использует ссылки после удаления.
Идентификаторы UID ("уникальные идентификаторы") действительно уникальны: они генерируются с помощью глобального счётчика и никогда не переиспользуются. Каждому объекту UID присваивается при создании и доступен через [datum.UID()]. Вы можете использовать UID как прямую замену \ref, заменив все вызовы locate(ref) в вашем коде на locateUID(ref). Использование этой системы обязательно для всех вызовов /Topic(, иначе DM будет выдавать ошибку. Используйте <a href='byond://?src=[UID()];'>, а не <a href='byond://?src=\ref[src];'>.
Хотя DM допускает и другие способы объявления переменных, для единообразия следует использовать именно этот.
Вы обязаны использовать табуляцию для отступов в коде, НЕ ПРОБЕЛЫ.
Пробелы разрешается использовать для выравнивания, но сначала сделайте отступ до уровня блока с помощью табуляции, а затем добавьте нужное количество пробелов.
"Костыльный" код — например, добавление специфических проверок вроде istype(src, /obj/whatever), крайне не рекомендуется и допускается только тогда, когда другого варианта действительно нет.
Подсказка: фраза "Я не смог сразу придумать нормального решения, значит другого варианта нет" здесь не сработает! Если вы не знаете, как решить задачу правильно — прямо скажите об этом и попросите помощи. Именно для этого и существуют мейнтейнеры.
Избегайте костылей, применяя объектно-ориентированные подходы: переопределяйте процедуры (procs) или выносите код в отдельные функции, которые можно переопределять при необходимости.
То же самое касается исправления багов: если в процедуру передаётся некорректное значение откуда-то, где его быть не должно, не исправляйте проблему внутри самой процедуры — устраните её в источнике, если это возможно.
Копирование кода из одного места в другое допустимо в небольших краткосрочных проектах, но наш билд — долгосрочный проект, и дублирование здесь крайне не рекомендуется.
Вместо этого используйте ООП или просто выносите повторяющийся код в отдельные функции.
Сначала прочитайте комментарии в этой теме на форуме BYOND, начиная с указанного места.
Здесь два ключевых момента:
-
Определение списка прямо в объявлении переменной вызывает скрытую процедуру
init. Если список нужно создавать при запуске — делайте это вNew()(или, что лучше, вInitialize()), чтобы избежать накладных расходов от двойного вызова (Init(), а затемNew()). -
Такой подход также потребляет больше памяти — список резервируется сразу, даже если объект, которому он принадлежит, никогда его не использует.
Помните: хотя такой компромисс часто оправдан, он не подходит для всех случаев. Внимательно подумайте, действительно ли он нужен в вашем случае.
Наш игровой контроллер хорошо справляется с длительными операциями и лагами, но он не может повлиять на то, что происходит при загрузке карты, когда вызывается New() для всех атомов на карте. Если вы создаёте новый атом — используйте процедуру Initialize() вместо New() для инициализации. Это уменьшит количество вызовов процедур при загрузке мира.
Обычно мы требуем обновлять устаревший код, если вы вносите правки рядом с ним. Однако это правило не распространяется на замену New() → Initialize(). Такие системы сильно зависят от иерархии наследования, и неосторожная модификация существующего кода может привести к багам, которые проявятся только через месяцы.
При объявлении параметров процедуры префикс var/ подразумевается автоматически. Не включайте его явно.
Пример:
// Плохо
obj/item/proc1(var/input1, var/input2)
// Хорошо
obj/item/proc1(input1, input2)Это означает использование переменных вроде mode = 1 или mode = 2 без пояснения, что они значат. Вместо этого создавайте #define с понятными именами.
Пример:
/datum/proc/do_the_thing(thing_to_do)
switch(thing_to_do)
if(1)
(...)
if(2)
(...)Непонятно, что означают 1 и 2. Лучше сделать так:
#define DO_THE_THING_REALLY_HARD 1
#define DO_THE_THING_EFFICIENTLY 2
/datum/proc/do_the_thing(thing_to_do)
switch(thing_to_do)
if(DO_THE_THING_REALLY_HARD)
(...)
if(DO_THE_THING_EFFICIENTLY)
(...)Это делает код читабельнее. Привыкайте так писать!
(if, while, for и т.д.)
- В одной строке с управляющей конструкцией не должно быть кода:
if(condition) return ...— запрещено. - При сравнении переменной с числом используйте форму
переменная оператор число, а не наоборот. Например:if(count <= 10), а неif(10 >= count). - Управляющие конструкции должны быть записаны как
if(), без пробела между ключевым словом и скобками.
Он же "early return", он же "ранний ретёрн".
Не оборачивайте всю процедуру в if-блок, если можно просто вернуться при невыполнении условия.
Пример:
// Плохо
/datum/datum1/proc/proc1()
if(thing1)
if(!thing2)
if(thing3 == 30)
сделать чё-то
// Хорошо
/datum/datum1/proc/proc1()
if(!thing1)
return
if(thing2)
return
if(thing3 != 30)
return
сделать чё-тоЭто предотвращает чрезмерную вложенность и облегчает читаемость кода.
Если вам нужно вызвать процедуру через определённое время — используйте addtimer() вместо spawn() и sleep() там, где это возможно.
Хотя это сложнее, это производительнее и, в отличие от spawn() или sleep(), может быть отменено.
Подробнее: tgstation/tgstation#22933.
Пример:
// Плохо
/datum/datum1/proc/proc1()
spawn(5)
do_thing(arg1, arg2, arg3)
// Хорошо
addtimer(CALLBACK(procsource, PROC_REF(do_thing), arg1, arg2, arg3), waittime, timertype)В коде игры много устаревшего кода, использование которого больше не приветствуется. Вот примеры:
-
Не используйте цветовые макросы (
\red,\blueи т.д.). Вместо этого —span-макросы. (span_warning("Красный текст"),span_notice("Синий текст")).// Плохо to_chat("\red Красный текст \black чёрный текст") to_chat("<span class='warning'>Красный текст</span>") // Хорошо to_chat("[span_warning("Красный текст")] чёрный текст")
-
При обращении к переменной/процедуре объекта не пишите
src.var/src.proc()—src.подразумевается автоматически:// Плохо var/user = src.interactor src.fillReserves(user) // Хорошо var/user = interactor fillReserves(user)
-
Входные данные от игроков всегда должны быть экранированы. Используйте
stripped_inputвместо обычногоinput. Считайте, что любой ввод от игрока — потенциально вредоносный. -
Запросы к базе данных должны использовать параметры (значения с
:). Это предотвращает SQL-инъекции.// Плохо var/datum/db_query/query_watch = SSdbcore.NewQuery("SELECT reason FROM [format_table_name("watch")] WHERE ckey='[target_ckey]'") // Хорошо var/datum/db_query/query_watch = SSdbcore.NewQuery("SELECT reason FROM [format_table_name("watch")] WHERE ckey=:target_ckey", list( "target_ckey" = target_ckey ))
-
Все вызовы
/Topic()должны проверяться на корректность. Клиенты легко могут подделать вызов, поэтому убедитесь, что он допустим для текущего состояния объекта. Не полагайтесь на UI! -
Информация, которую игроки могут использовать для метаигры (например, определение типа антагониста или стадии раунда), должна быть доступна только администраторам.
-
Функции, способные вызвать масштабные изменения или хаос (вкладка "Веселье"), должны быть изначально заблокированы за одной из стандартных админ-ролей. Выбирайте роль по уровню потенциального ущерба.
-
Поскольку ошибки времени выполнения не показывают полный путь, старайтесь избегать файлов с одинаковыми именами в разных папках.
-
Имена файлов не должны содержать заглавных букв, пробелов или символов, требующих экранирования в URI.
-
Все файлы и пути, на которые есть ссылки в коде (кроме
#include), должны быть строго в нижнем регистре, чтобы избежать проблем на чувствительных к регистру файловых системах.
-
Код должен быть модульным: если вы добавляете новый функционал — он должен идти в отдельный файл, если для него нет подходящего существующего файла (например, новый инженерный инструмент имеет смысл добавить в
tools.dm, а не создавать новый файл с нуля). -
Раздутый код иногда необходим, но в любом случае, перед добавлением нового функционала хорошо подумайте — стоит ли оно того? Вам будет гораздо легче дать ответ на данный вопрос, если вы делаете свой код модульным.
-
Поддерживайте свой код: если появятся ошибки — вас первым попросят исправить это. Конечно, некоторые возникающие в последствии баги и недочёты могут быть слишком трудными конкретно для вас и в таком случае стоит попросить кого-то более опытного о помощи. Но тем не менее, брать ответственность за сопровождение своего кода — правило хорошего тона.
-
Если вы использовали регулярные выражения ("regex") для поиска и замены кода при рефакторинге, укажите их в описании PR'а. Это может существенно упростить дальнейшую работу с затронутым кодом для будущих разработчиков.
Как любой другой язык программирования, Dream Maker имеет свои особенности, нюансы и фишки. Знание некоторых из них может быть очень полезно, к примеру:
for(var/i = 1, i <= some_value, i++) — это стандартный способ написания возрастающего цикла в большинстве языков (особенно "C"-подобных). Однако, существующий в DM вариант вида for(var/i in 1 to some_value) работает быстрее, как ни странно. Соответственно, лучше используйте DM синтаксис в целях производительности.
Обратите внимание: to включает последнее значение, т.е. эквивалентно <=. Для использования < логики пишите 1 to some_value - 1.
НО, если some_value или i изменяются внутри тела цикла или если длина итерируемого списка изменяется во время выполнения цикла, использование цикла данного вида НЕВОЗМОЖНО!
Первый вариант быстрее второго.
Результаты тестирования можно посмотреть здесь: https://file.house/zy7H.png. Использованный для тестирования код в читабельном виде: https://pastebin.com/w50uERkG
Если вы хотите итерировать какой-либо список с помощью цикла и при это уверены, что все объекты в списке имеют конкретный тип, мы можем оптимизировать работу данного списка, избавившись от какой-либо проверки на тип, не имеющий смысла.
То есть:
var/list/bag_of_swords = list(sword, sword, sword, sword)
// Плохо
for(var/obj/item/sword/S in bag_of_swords)
...
// Плохо
for(var/s in bag_of_swords)
if(istype(s, /obj/item/sword))
var/obj/item/sword/S = s
...
// Хорошо
for(var/s in bag_of_swords)
var/obj/item/sword/S = s
...Очевидно, если в списке будет объект отличного типа, то его обработка закономерно приведёт к рантаймам. В общем смысле, при использовании такой конструкции проверка на тип перекладывается с DM на кодера.
Как и в других "C"-подобных языках, в DM существует оператор ., он же "dot-operator". Он используется для доступа к полям (переменным) и методам (функциям) экземпляров объекта.
Пример:
var/mob/living/carbon/human/H = YOU_THE_READER
H.gib()Однако, в случае DM . может иметь форму переменной, по умолчанию имеющей null значение. Особенность . заключается в том, что оно автоматически возвращается (return) в конце процедуры (proc), если явный return не был указан ранее в теле процедуры. И что тут особенного, спросите вы?
Как вы могли догадаться, оператор ., существующий в каждой процедуре, может быть использован как временная переменная. Хоть такая переменная не может заменить приведённую к типу переменную, она может хранить в себе значение, как и любой другой var в DM, хотя и прямой доступ как к обычной переменной невозможен.
. совместима с рядом операторов, таких как:
. = 5
.++ // . == 6
.[1] // если . — список
В DM есть ключевое слово global, но оно не делает переменную глобальной в обычном смысле. Это слово означает, что конкретная переменная является общей для всех экземпляров данного типа (как static в C++/Java)
Пример:
/mob
var/global/thing = TRUEПомимо этого, также существует незадокументированное ключевое слово static, которое делает то же самое, но лучше отражает его суть. Поэтому всегда используйте static вместо global.
А теперь о "по-настоящему" глобальных переменных. Они должны быть заданы с помощью #define в code/DEFINES/globals.dm. Примеры ниже.
Объявление глобальной переменной:
GLOBAL_VAR(my_global_here)Доступ к ней:
GLOB.my_global_here = X
Также есть:
- GLOBAL_VAR_INIT(name, value) — с начальным значением переменной.
- GLOBAL_LIST_INIT — для объявления списков.
- И так далее.
- Разделяемые пробелами операторы:
- Булевы и логические операторы:
&&,||,<,>,==и т.д. (но не!) - Побитовое И:
& - Разделители аргументов:
,(и;вfor-циклах) - Операторы присваивания:
=,+=и т.д. - Математические операторы:
+,-,/,*
- Булевы и логические операторы:
- Неразделяемые пробелами операторы:
- Побитовое ИЛИ:
| - Операторы доступа:
.,: - Скобки:
() - Логическое НЕ:
!
- Побитовое ИЛИ:
- Побитовое И -
&- Пишите как
bitfield & bitflag, но НИКОГДА неbitflag & bitfield. Хотя оба варианта работают, обратный порядок нестандартен и запутывает.
- Пишите как
- В ассоциативных списках ключи-строки должны быть в кавычках:
- ПЛОХО:
list(a = "b") - ХОРОШО:
list("a" = "b")
- ПЛОХО:
-
Мы предпочитаем использовать побитовые сдвиги вместо прямого указания чисел.
// Хорошо #define MACRO_ONE (1<<0) #define MACRO_TWO (1<<1) #define MACRO_THREE (1<<2) #define MACRO_ALL (~0) // Плохо #define MACRO_ONE 1 #define MACRO_TWO 2 #define MACRO_THREE 4 #define MACRO_ALL 7 // или 16777215 как более точное значение
Это делает код читабельнее и снижает вероятность ошибок.
-
Не используйте сокращённый синтаксис
INSERTбез указания названий колонок — это ломает запросы при малейших изменениях схемы и мешает использовать таблицы извне (например, на сайте). -
Используйте параметры в запросах (упомянуто в "Пишите безопасный код")
-
Всегда проверяйте успешность выполнения
if(!query.warn_execute()). Используя данный стандартный формат вы можете быть уверены, что используются корректные сообщения для логов. -
Всегда вызывайте
qdel()для запросов после использования — это освобождает память. -
Все изменения схемы БД должны быть задокументированы в
changelogбазы данных в SQL и отражены в файлах схемы. -
При любом изменении схемы увеличивайте define
SQL_VERSIONи обновляйте пример конфига, а также добавляйте скрипт миграции в папкуSQL/updates. -
Запросы никогда не должны указывать название базы данных — ни в коде, ни в файлах.
Некоторые части кода написаны на Rust по соображениям производительности и надёжности:
- Атмосферный движок, "MILLA", находящийся в директории
rust/src/milla/. - Модуль
mapmanipот "Aurora Station" используемый для автоматизации измененияDMM-файлов, находящийся в директорииrust/src/mapmanip.
Все Rust-компоненты в билде компилируются в единую библиотеку, отдельную от остального кода. Если вы используете Windows, то по умолчанию получаете уже собранную версию. Если вы на Linux — вы уже собирали её самостоятельно, чтобы запустить сервер.
Если вы вносите изменения в Rust-библиотеку, вам нужно будет пересобрать её. Процесс почти идентичен тому, что используется в проекте rust-g. Единственное отличие — команду cargo следует запускать из директории rust/, при этом указывать флаг --all-features не обязательно (хотя это и не повредит).
Сервер автоматически обнаружит вашу локальную сборку и будет использовать её вместо стандартной Windows-версии.
Когда будете готовы создавать PR, НЕ ИЗМЕНЯЙТЕ файлы rustlibs.dll или tools/ci/librustlibs_ci.so. Оставьте опцию "Allow edits and access to secrets by maintainers" включённой и оставьте следующий комментарий в своём PR: !build_rust. Бот автоматически соберёт нужные файлы и обновит вашу ветку.
Перед созданием PR обязательно запустите Map Merge, чтобы убедиться в том, что разница файлов карт при изменении будет минимальной. Даже если вы использовали сторонние программы для маппинга (например, "StrongDMM").
- Локальные правки переменных атомов через маппинг-редактор недопустимы. Если вы хотите изменить базовое поведение объекта — обязательно создавайте новый сабтайп в коде. Это упрощает изменение всех экземпляров атома одновременно и избавляет от необходимости искать и изменять каждый экземпляр вручную.
- Сабтайпы, предназначенные только для away-миссий, гейтов и подобного, должны находиться в
.dmфайлах, названия которых соответствуют названию конкретной карты, хранящейся вcode\modules\awaymissionsилиcode\modules\ruins. Это упрощает удаление ненужных объектов при удалении карты, снижая вероятность оставить мусор в коде. - Убирайте "грязные" переменные после редактирования (например,
pixel_x = 0) если значение переменной совпадает с дефолтным. - Не редактируйте зоны (
area) через "var-editing" непосредственно на карте. Все зоны с одинаковыми типами (даже с изменёнными вручную переменными) DM'ом расцениваются как одна и та же зона, что может привести ко многим проблемам (например, ломаются подсистемы событий или системы электросетей, которые очень трудно отлаживать).