Skip to content

Latest commit

 

History

History
463 lines (365 loc) · 18.1 KB

CODE_CONVENTION.md

File metadata and controls

463 lines (365 loc) · 18.1 KB

Соглашения о коде

Note

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

Введение

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

В случае если что-то не было здесь оговорено, вы можете уточнить этот момент в конфе. Хотя скорее всего это значит, что в том, что вас интересует вы вольны думать самостоятельно.

Пробелы и отступы

Блоки кода

В качестве отступов для компилируемых блоков кода используются только табы. Если код относится к комментарию (то есть для примера), то выравнивать рекомендуется пробелами.

Процедуры и функции

Между вызовом процедуры/функции и скобками не должно быть пробела.

//Плохо:
/proc/foo()
	return function (1)

//Хорошо:
/proc/foo()
	return function(1)

Операторы управления (if, while, for и т.д.)

Не должны писаться одной строкой. Иногда, если так действительно будет лучше, допускается однострочные switch и else if конструкции.

//Плохо:
if(something) return TRUE

for(var/i in something) i.foo()

//Хорошо:
if(something)
	return TRUE

for(var/i in something)
	i.foo()

//Допустимо:
switch(x)
	if(case_1) foo()
	if(case_2) return FALSE

Оператор членства (in)

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

//Плохо:
if(foo && bar in list)

if(bar in arr and foo)

//Хорошо:
if(foo && (bar in list))

if((bar in arr) and foo)

Все остальные операторы (+, -, =, &&, || и т.д.)(и in)

Один пробел перед оператором и один пробел после. Исключение - побитовые. Пробелы при их использовании вставляйте на своё усмотрение.

//Плохо:
var/a=1
var/b   = 2
var/c =a+b

//Хорошо:
var/a = 1
var/b = 2
var/c = a + b

Скобки и запятые

Следуйте единому формату. Так или иначе пробел после запятой - обязателен.

//Плохо:
list( 1,2, 3, 4 ,5)

//Хорошо:
list(1, 2, 3, 4, 5)

//Допустимо:
list( 1 , 2 , 3 , 4 , 5 )

Комментарии

Форматирование только через пробелы. Обратите внимание на выравнивание.

//Плохо:
#define A "something"		//	Это что-то.
#define B "anything"	//Это что угодно.

//Хорошо:
#define A "something"  // Это что-то.
#define B "anything"   // Это что угодно.

Выравнивание новых строк

Например, при объявлении элементов списка - преимущественно пробелами.

//Плохо:
list(1, 2, 3,
	4, 5, 6)

//Хорошо:
list(1, 2, 3,
     4, 5, 6)

//Допустимо:
list(
	1, 2, 3,
	4, 5, 6
)

Описание путей

Абсолютные

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

//Плохо:
obj
	var
		varname1 = 0
		varname2
	proc
		proc_name()
			code
	item
		weapon
			name = "Weapon"
			proc
				proc_name2()
					..()
					code

//Хорошо:
/obj
	var/varname1 = 0
	var/varname2

/obj/proc/proc_name()
	code

/obj/item/weapon
	name = "Weapon"

/obj/item/weapon/proc/proc_name2()
	..()
	code

Должны начинаться с /

//Плохо:
mob/living

//Хорошо:
/mob/living

Не описываются через текстовую строку

//Плохо:
var/path = "/obj/item/something"

//Хорошо:
var/path = /obj/item/something

Записывать путь объекта строкой небезопасно, так как в случае если каким-то образом тот изменится или вообще удалится из билда, компилятор не сообщит о попытке использования несуществующего типа.

Работа с кодом

Объявление аргументов в процедурах

Не должно быть конструкций типа var/ и as ...

//Плохо:
/proc/foo(var/atom/A as area|turf|obj|mob)

//Хорошо:
/proc/foo(atom/A)

Оператор :

Использование запрещено. Всегда приводите объект к нужному вам типу.

//Плохо:
/proc/foo(atom/A)
	return A:some_var

//Хорошо:
/proc/foo(atom/A)
	var/obj/O = A
	return O.some_var

Данный оператор отрабатывает в рантайме, проходя по всем подтипам объекта в поисках запрашиваемой переменной, соответственно его использование чревато проблемами производительности.

proc().var и list[index].var - подобные конструкции равнозначны использованию : и их также не следует использовать по тем же причинам.

//Плохо:
var/count = foo().len

//Хорошо:
var/list/L = foo()
var/count = L.len

Магические числа

Если не знаете что это, то прочитайте эту статью Магическое число (программирование). Соответственно их быть не должно. Используйте дефайны или константные переменные.

//Плохо:
/proc/foo(variable)
	switch(variable)
		if(1)
			do something
		if(2)
			return

//Хорошо:
#define CASE_1 1
#define CASE_2 2

/proc/foo(variable)
	switch(variable)
		if(CASE_1)
			do something
		if(CASE_2)
			return

#undef CASE_1
#undef CASE_2

Булевский тип

Вместо 1 и 0 используйте TRUE и FALSE соответственно.

Использование src

Используйте только тогда, когда это необходимо.

//Плохо:
/mob/some/class/proc/foo()
	src.some_var = some_value
	src.another_var = another_value

//Хорошо:
/mob/some/class/proc/foo()
	some_var = some_value
	another_var = another_value

//Пример необходимого использования:
/mob/some/class/proc/foo(some_var, another_var)
	src.some_var = some_var
	src.another_var =  another_var

Избегайте ненужных проверок типов в циклах

Приведение типов в циклах for(var/some/foo in bar) содержит внутреннюю проверку istype(), которая отсеивает неподходящие типы, включая null. Ключевое слово as anything позволяет пропустить ненужные проверки.

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

Обычно, null в листе означает, что как-то неправильно были обработаны ссылки, что затрудняет отладку хард делов.

var/list/bag_of_atoms = list(new /obj, new /mob, new /atom, new /atom/movable, new /atom/movable)
var/highest_alpha = 0

//Плохо:
for(var/atom/thing in bag_of_atoms)
	if(thing.alpha <= highest_alpha)
		continue
	highest_alpha = thing.alpha


//Хорошо:
for(var/atom/thing as anything in bag_of_atoms)
	if(thing.alpha <= highest_alpha)
		continue
	highest_alpha = thing.alpha


//Допустимо:
for(var/atom in bag_of_atoms)
	var/atom/thing = atom
	if(thing.alpha <= highest_alpha)
		continue
	highest_alpha = thing.alpha

process() процедуры должны быть зависимы от таймингов сабсистем и использовать seconds_per_tick

На простом примере, если мы хотим, чтобы моб терял 2 жизни в секунду:

//Плохо:
/mob/testmob
	var/health = 100
	var/health_loss_per_tick = 4 //Мы хотим терять 2 здоровья в секунду

/mob/testmob/process() //сабсистема SSmobs запускается каждые 2 секунды
	health -= health_loss_per_tick

//Хорошо:
/mob/testmob
	var/health = 100
	var/health_loss_per_second = 2

/mob/testmob/process(seconds_per_tick)
	health -= health_loss_per_second * seconds_per_tick

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

Это не учитывает тайминги перед тиком сабсистмы и становится проблемой, если кто-то захочет изменить скорость работы (wait) сабсистемы мобов. Например, если сабсистема станет запускаться каждую секунду (в два раза быстрее), то и моб начнет терять жизни в 2 раза быстрее: по 4 в секунду вместо 2-х. И наоборот, если сабсистема будет запускаться реже.

Решением является использование seconds_per_tick. Сабсистема сама сообщает в process(), как часто она запускается.

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

Аналогично с prob() в процессинге:

//Плохо:
/mob/testmob/process()
	if(prob(10)) // 10% шанса на событие в тик, когда тик может быть разным
		event()

//Хорошо:
/mob/testmob/process(seconds_per_tick)
	if(SPT_PROB(10, seconds_per_tick)) // 10% шанса на событие в секунду
		event()

SPT_PROB конвертирует шанс на шанс в секунду и обеспечивает более постоянное значение, зависимое от скорости сабсистемы.

Стоит понимать, что seconds_per_tick - идеальное "среднее" значение скорости сабсистемы, оно не учитывает возможные лаги игры и замедление работы сабсистем из-за этого.

Особенности BYOND

Использование spawn()

Не используйте. Причины:

  • Первая проблема спавна - вопреки всеобщему мнению, считает в тиках процессора, а не игровых тиках.
    • spawn(10), сюрприз-сюрприз вовсе не одна секунда.
  • Вторая проблема - ссылки в ассинхронном вызове. Если вы ссылаетесь на объект, который под спавном, и что-то в этом время удалит этот объект(Попытается), то внутри спавна останется ссылка на него.
  • Третья проблема - отсутствие возможности профилизировать вызванный под спавном код. В профайлере он будет подписываться как "ASYNC FUNCTION CALL", или что-то подобное, что вообще ничего не говорит о том что это за функция такая.
  • Четвёртая проблема - отсутствие контроля над выполнением. В случае с, к примеру, таймером, которые вынесены в отдельную подсистему - можно выбирать приоритет выполнения таймера, в то время как spawn() является низкоуровневым byond-проком, не контролируется нами, и может работать не всегда оптимальным для нас способом.

В зависимости от того, как используется спавн есть два пути замены:

  • Если spawn(time):
    • Как правило, такие спавны заменяются на addtimer(CALLBACK(thingtocall, TYPE_PROC_REF(thingtocall_path, proc_name), args), time)
    • Если с помощью спавна лишь изменяется переменная датума, то лучше использовать VARSET_IN(datum, var, var_value, time).
  • Если spawn() или spawn(0):
    • Если спавн содержит единственный прок, то просто оберните его в INVOKE_ASYNC(thingtocall, TYPE_PROC_REF(thingtocall_path, proc_name), args). Иначе перенесите всё содержимое спавна в новый прок, который уже и добавите в INVOKE_ASYNC(thingtocall, TYPE_PROC_REF(thingtocall_path, proc_name), args)
    • Если всё содержимое прока обернуто в спавн, то в самом проке прописать set waitfor = FALSE

Примеры замен под спойлерами ниже:

Пример замены spawn(time)
//Плохо:
/mob/some/class/proc/foo()
	code
	spawn(20)
		switch(variable)
			if(1)
				do_something
			if(2)
				return

	spawn(40)
		other_mob.do_something_crazy(a, b)

	spawn(30)
		name = "Steve"

//Хорошо:
/mob/some/class/proc/foo()
	code
	addtimer(CALLBACK(src, PROC_REF(do_something_wrapper), variable), 20)
	addtimer(CALLBACK(other_mob, TYPE_PROC_REF(/mob, do_something_crazy), a, b), 40)
	VARSET_IN(src, name, "Steve", 30)

/mob/some/class/proc/do_something_wrapper(variable)
	switch(variable)
		if(1)
			do_something
		if(2)
			return
Пример замены spawn(0)
//Плохо:
/mob/some/class/proc/foo()
	spawn(0)
		switch(variable)
			if(1)
				do_something
			if(2)
				return

//Хорошо:
/mob/some/class/proc/foo()
	set waitfor = FALSE

	switch(variable)
		if(1)
			do_something
		if(2)
			return
//Плохо:
/mob/some/class/proc/foo()
	code
	spawn(0)
		switch(variable)
			if(1)
				do_something
			if(2)
				return

//Хорошо:
/mob/some/class/proc/foo()
	code
	INVOKE_ASYNC(src, PROC_REF(do_something_wrapper), variable)

/mob/some/class/proc/do_something_wrapper(variable)
	switch(variable)
		if(1)
			do_something
		if(2)
			return

//Хорошо:
/mob/some/class/proc/foo()
	code
	do_something_wrapper(variable)

/mob/some/class/proc/do_something_wrapper(variable)
	set waitfor = FALSE

	switch(variable)
		if(1)
			do_something
		if(2)
			return

Работа с циклами (black magic)

Цикл вида for(var/i = 1; i <= const_val; i++) должен быть записан в следующей форме for(var/i in 1 to const_val). Причина: второй цикл работает быстрее. Почему? Хороший вопрос, стоит задать его кодерам BYOND. У обоих видов одинаковое поведение, но у второго есть нюансы. to тоже самое, что и <=, соответственно чтобы было < нужно сделать for(var/i in 1 to const_val - 1). Ещё важный момент - const_val переменная не должна быть изменена во время цикла. То есть если вы делаете проход по списку, который изменяется внутри цикла, а const_val размер списка, то вам нужно использовать классический, первый, вариант.