Перейти к контенту

Рефакторинг: _g.script


Рекомендуемые сообщения

Поехали.

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

Сейчас я начну переписывать скрипт прямо "по живому", так что ревью, тесты и дополнения приветствуются, и, надеюсь, общими усилиями что-нибудь все-таки сделаем.

первый кусок:

 

if ( jit == nil ) then profiler.setup_hook() end

string_sub, string_gsub = string.sub, string.gsub
string_find, string_gfind = string.find, string.gfind
string_match, string_gmatch = string.match, string.gmatch
string_format, string_char = string.format, string.char
string_len = string.len	-- dc: а # - некошерно ?

table_insert, table_remove, table_foreach = table.insert, table.remove, table.foreach

math_random, math_randomseed = math.random, math.randomseed
math_sin, math_cos, math_acos = math.sin, math.cos, math.acos
math_floor, math_ceil = math.floor, math.ceil
math_mod, math_sqrt = math.mod, math.sqrt

function round( value )
	local min = math_floor( value )
	local max = min + 1
	-- if value - min > max - value then return max end	-- dc: приведем к "<, ==, >="
	if ( value - min ) < ( max - value ) then return min end
	return max
end


function to_str( v )
	if type( v ) == "userdata" then
		local n = v.name
		return ( type( n ) == "function" and ( "userdata[" .. v:name() .. "]" ) )
			or ( type( n ) == "string" and ( "userdata[" .. n .. "]" ) )
			or "*userdata*"
	elseif v == nil then return "<nil>"	-- dc: именно "<nil>" - для извращенцев
	end
	return tostring( v )
end


function log( ct, fmt, ... )	-- src - откуда вызван, если false - не выводится, категория игнорируется
	-- get_console():execute( "load ~" .. ct .. "~ [_g] " .. ( fmt or "" ) )	-- для поиска потерянных аргументов и скрипта, их потерявшего
	get_console():execute( "load ~" .. ct .. "~ [_g] " .. ( ( ... and string_format( fmt, ... ) ) or fmt or "" ) )
	if ct == "error" or ct == "log" then get_console():execute( "flush" ) end
end


function abort( fmt, ... )
	get_console():execute( string_format( "load ~error~: %s", ( ... and string_format( fmt, ... ) ) or fmt or "" ) )
	get_console():execute( "load ~~~ Обнаружена ошибка. Описание ошибки смотрите выше. Игра остановлена." )
	get_console():execute( "load ~~~ Пожалуйста, не надо сообщать об ошибке в строке 50." )
	get_console():execute( "load ~~~ Какую-либо ценность имеют 10 строк ДО этого сообщения." )
	get_console():execute( "flush" )
	local a ; a = 1/a; get_console():execute( "quit" )
	exit()
end

 

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

function round( value ) -- округление, которого не хватает в библиотеках. Там где-то в файле такая же болтается, но ее находим, выносим в начало, и переписываем, чтобы больше перед глазами не маячила, когда займемся прочим.
function to_str( v ) -- преобразование аргумента в строку. Используется потом при работе с логикой, и где-то еще. nil и userdata на вход подавать НЕЛЬЗЯ, что бы там пысы не писали. Точнее, nil - еще можно, из логики, а userdata просто повиснет, или вылетит с бредом в логе. Это функция именно для логики. Для наших собственных нужд потом добавим такую же в отдельном скрипте. Упс ! Я ее, оказывается, уже успел переписать. Но тем не менее, все равно для логики.

log() и abort() - отладочные. Соответственно, вывести что-то в лог и крэшнуть игру. Как и в случае с to_str() Здесь используются короткие, "world-wide совместимые" варианты. Более полезные определяются в более другом скрипте. А здесь главное - успеть вывести хоть что-то вменяемое, если где-то отработало что-то, до чего пока не добрались.

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

Изменено пользователем Dennis_Chikin
Ссылка на комментарий

 

 

Сейчас я начну переписывать скрипт прямо "по живому", так что ревью, тесты и дополнения приветствуются, и, надеюсь, общими усилиями что-нибудь все-таки сделаем.

На заметку академикам: вот это именно то, что я и мечтал видеть, и только попробуйте Дениса не поддержать продуманными и содержательными постами ... :russian_ru:

  • Спасибо 1
Ссылка на комментарий

Продолжаем разговор:

 

sys_ini = system_ini()
global_time_ms, game_time_ms, game_time_sec, game_time_time = 0, 0, 0, 0
game_time_start, game_time_base = 0, 0	-- игровое время на начало игры, игровое время при загрузке

dist_sw_online = 150
dist_sw_offline = 180

IAmAStalker, IAmAMonster = {}, {}		-- obsolete
IAmAWeapon, IAmAWeaponFireOnly = {}, {}		-- obsolete

c_npc = false
c_actor = false
c_mob = {}
c_ai = {}
c_trader = false
c_ibox = false
c_wpn = {}
c_arms = {}

mob_online = {}		-- табличка принудительного on/off-line для монстров
force_offline = {}	-- принудительный оффлайн для неписей и тайников

function printf() end

function print_table() end

function trim( s ) return string_match( s, "^%s*(.*%S)" ) end	-- обрезаем пробелы


function Parse_StrToTbl( s, div, mode )	-- ( строка, разделитель, [true|число|nil для строк] )
	local t = {}
	local pattern
	if div then pattern = "%s*([^" .. sDiv .. "]+)%s*"
	else pattern = "[_%w]+"
	end
	if mode then
		if mode == true then for v in string_gmatch( s, pattern ) do t[v] = true end	-- таблица вида [значение] = true
		elseif type( mode ) == "number" then					-- таблица вида [idx] = число
			for v in string_gmatch( s, pattern ) do table_insert( t, tonumber( v ) or v ) end
		else abort( "Parse_StrToTbl, ivalid mode: %s", tostring( mode ) )
		end
	else for v in string_gmatch( s, pattern ) do table_insert( t, v ) end
	end
	return t
end


function parse_custom_data( str )	-- комментарии в профилях и cd не использовать ! Сохраняются в cd как есть !
	local t = {}
	if str then
		--log( "info", "parse_custom_data: [" .. string_sub( str, 1, 150 ) .. "]" )
		local ts
		for sect, data in string_gfind( str, "%s*%[([^%]]*)%]%s*([^%[%z]*)%s*" ) do
			--log( "info", "parse_custom_data, sect: [" .. sect .. "], data: [" .. string_sub( data or "nil", 1, 120 ) .. "]" )
			ts = {}
			t[sect] = ts
			for s in string_gfind( data or "", "([^\n]*)\n*" ) do
				if string_find( s, "=" ) then
					for k, v in string_gfind( s, "([^=]-)%s*=%s*(.*)" ) do
						--log( "info", "parse_custom_data, " .. k .. " = " .. string_sub( v or "nil", 1, 120 ) )
						if v then
							k = string_match( k, "^%s*(.*%S)" )
							if k and k ~= "" then
								ts[k] = string_match( v, "^%s*(.*%S)" )
					end	end	end
				else
					local k = string_match( s, "^%s*(.*%S)" )
					if k and k ~= "" then ts[k] = "<<no_value>>" end
	end	end	end	end
	return t
end


function gen_custom_data( t )
	local s = ""
	for k, v in pairs( t ) do
		s = s .. "[" .. k .. "]\n"
		for kk, vv in pairs( v ) do
			if vv ~= "<<no_value>>" then s = s .. kk .. " = " .. vv .. "\n"
			else s = s .. kk .. "\n"
	end	end	end
	return s
end


function parse_names( s )
	local t, n = {}, 0
	for name in string_gfind( s, "([%w%-%._\\]+)[%,%s]*" ) do n = n + 1; t[n] = name end
	return t
end


function parse_key_value( s )
	if s then
		local t = {}
		local k
		for n in string_gfind( s, "([%w%-%._\\]+)[%,%s]*" ) do
			if k then t[k] = n; k = nil
      			else k = n
		end	end
		return t
	end
end


function parse_nums( s )
	local t, n = {}, 0
	for entry in string_gfind( s, "([%d%.]+)%,*" ) do n = n + 1; t[n] = tonumber( entry ) end
	return t
end

sys_ini = system_ini() -- файлы в винде вообще открываются ОЧЕНЬ медленно. А сам сталкер еще и что-то с ними делает, тоже мееедленно, и печально.

Отсюда и загрузка солянки по 2 минуты. Так что открываем все один раз, и так в открытом состоянии и держим.

 

global_time_ms, game_time_ms, game_time_sec, game_time_time = 0, 0, 0, 0

game_time_start, game_time_base = 0, 0 -- игровое время на начало игры, игровое время при загрузке

 

В общем, вместо времени 64 бит с многочисленными преобразованиями в таблицы и обратно я использую везде 32.

Ага, 31 мая, 32 мая, и т.д. ;) Чтобы такого не было - время всегда считается не от НИ, а от загрузки. И сохраняется дельта. А месяц топтаться на одной оркации без единого сэйва - вряд-ли кто будет.

Ну и наконец, вместо медленного вызова функций и пересчета используем быстрое обращение к переменной. А обновляются они раз в 200ms - вполне достаточно, я считаю.

 

dist_sw_online = 150

dist_sw_offline = 180

Опять же, чтобы не перечитывать и не пересчитывать постоянно - переменные. А из alife.ltx берем только при загрузке.

 

 

IAmAStalker, IAmAMonster = {}, {} -- obsolete

IAmAWeapon, IAmAWeaponFireOnly = {}, {} -- obsolete

 

c_npc = false

c_actor = false

c_mob = {}

c_ai = {}

c_trader = false

c_ibox = false

c_wpn = {}

c_arms = {}

 

- то, которое устарело - это была одна из солянковских оптимизаций. Устарело. Просто не от всюду еще дочистил. Добавлены константы и таблички с классами. Инициализируются при старте, когда отработает class_regisrator и появится alife().

 

mob_online = {} -- табличка принудительного on/off-line для монстров

force_offline = {} -- принудительный оффлайн для неписей и тайников

Как бы понятно из названия. Единые таблицы для ВСЕХ скриптов, где применяется такой фокус. Хотя вообще-то на самом деле это плохой, негодный фокус, и он не нужен, но это уже другая тема. А так - в amk было переодевание и тайники, использовавшее convert_npc={}, еще где-то - need_be_online={} в se_monster, не знаю, зачем... Тысячи их. Все снести, где реально надо - заменить на единые глобальные.

 

printf() и print_table() - затычки для недовычищенного и прочих разных гибридизаций ежа с ужом через 50 скриптов, гордо именуемых "адаптациями".

 

И, наконец, преобразования разных забавных строк в разные забавные таблицы, и обратно. Часть - была и в оригинале, часть - во вножестве экземпляров в разных скриптах, и оригинальных, и которые см. про "адаптацию".

Все собрано в одном месте и переписано.

 

Тут важно понимать, что в LUA НЕТ инлайна, а следовательно, все равно все это будет работать медленно и печально, и прежде чем использовать какую-нибудь функцию преобразования чего-попало во что-нибудь еще, особенно, в цикле или апдейте - подумайте, а надо ли ?

Ибо нет ничего более нелепого, и вызывающего содрогание, чем конструкция вида:

local s = "1, 2, 3"

local t = parse_nums( s )

local n1, n2, n3

for k, v in pairs( t ) do if k == 1 then n1 = v end; if k == 2 then n2 = v end; if k == 3 then n3 = v end; end

Изменено пользователем Dennis_Chikin
  • Не согласен 3
Ссылка на комментарий
sys_ini = system_ini() -- файлы в винде вообще открываются ОЧЕНЬ медленно. А сам сталкер еще и что-то с ними делает, тоже мееедленно, и печально.

Тут ошибка. Сталкер загружает конфиги однократно, более их не выгружает, соответственно любые манипуляции с конфигами в скриптах не приводят к открытию каких-либо файлов. Более того, функция system_ini() есть возврат указателя CInifile *pSettings, что опять же не только не приводит ни к каким файловым действиям, но и с точки зрения производительности не стоит практически ничего.

Т.е., код сам по себе как код, только разницы никакой - что так, что получать system_ini в произвольном месте в скриптах. Тут скорее вопрос стиля кодирования и здесь я целиком за.

Изменено пользователем KD87
  • Нравится 1
  • Согласен 1
Ссылка на комментарий

"возврат указателя CInifile *pSettings"

 

Странно... Впрочем, все равно собирался стресс-тесты устраивать, заодно и проверю. Но ведь народ-то тоже подтверждает наличие эффекта от устранения пессимизации c system_ini() по каждому чиху...

Ссылка на комментарий

Ага:

! Cannot find saved game ~test~ [bind_stalker] ini1: 13829.931640625

! Cannot find saved game ~test~ [bind_stalker] sys_ini: 7527.0874023438

! Cannot find saved game ~test~ [bind_stalker] test: 517023.3125

! Cannot find saved game ~test~ [bind_stalker] test_ini: 4946.7431640625

тупо копипастой наплодил:

 

	local pt1 = profile_timer()
	local ini1, test
	pt1:start()
	for i = 1, 10000 do
		ini1 = system_ini()
		test = ini1:r_bool( "test1", "bool_t" )
	end
	pt1:stop()
	log( "test", "ini1: %s", pt1:time() )
	local pt1 = profile_timer()
	local ini1, test
	pt1:start()
	for i = 1, 10000 do
		test = sys_ini:r_bool( "test1", "bool_t" )
	end
	pt1:stop()
	log( "test", "sys_ini: %s", pt1:time() )

	local pt1 = profile_timer()
	local ini1, test
	pt1:start()
	for i = 1, 10000 do
		ini1 = ini_file( "test.ltx" )
		test = ini1:r_bool( "test1", "bool_t" )
	end
	pt1:stop()
	log( "test", "test: %s", pt1:time() )
	local pt1 = profile_timer()
	local ini1 = ini_file( "test.ltx" )
	local test
	pt1:start()
	for i = 1, 10000 do
		test = ini1:r_bool( "test1", "bool_t" )
	end
	pt1:stop()
	log( "test", "test_ini: %s", pt1:time() )

 

Собственно, вызов жрет. А вот с просто файлом, не system.ltx - форменный кошмар (это из 5 строк файл).

Изменено пользователем Dennis_Chikin
Ссылка на комментарий

@Dennis_Chikin, а может не вызов жрёт, а использование лишней переменной? Попробуй сделать так:

for i = 1, 10000 do
	test = system_ini():r_bool( "test1", "bool_t" )
end
Ссылка на комментарий

! Cannot find saved game ~test~ [bind_stalker] ini1: 13623.260742188

! Cannot find saved game ~test~ [bind_stalker] system_ini(): 12739.622070313

! Cannot find saved game ~test~ [bind_stalker] sys_ini: 7462.7309570313

 

Лучше, но не на много.

Ссылка на комментарий

Это торговлю-то с 17500 костюмами ? (Сорри за малопонятный большинству юмор.)

 

Правильнее на самом деле уменьшить общее количество как конфигов, так и их объем. Многие просто не нужны, и тянутся/плодятся, по тому, как сказал ZD, что в степи стоит 10 метров стены с вмятинами от голов разработчиков из ПЫС.

 

А вынос system_ini() в _g.script - это ловля 2-х микросекунд, но, в общем, их может быть много. Внезапно много.

Изменено пользователем Dennis_Chikin
Ссылка на комментарий

@Карлан, если заинклудить туда, то файл будет загружен только раз и время, затрачиваемое на его загрузку перенесется на старт игры. Но! Обращение к секциям в файле должно будет идти через system_ini(), никаких ini_file(). Вызов ini_file() ведет к чтению файла с диска в любом случае, насколько я понимаю.

Ссылка на комментарий

sys_ini = system_ini()

Что "sys_ini = system_ini()", что и "оптимизации" навроде "math_random = math.random()" - никогда не видел профита от них. Тогда смысл? Изменено пользователем Shadows
Ссылка на комментарий

Смысл - ровно в 2 раза. Не, там, где за один раз в игру - не жалко этих 2-х микросекунд.

Но ведь отдельные гении ухитряются писать циклы наподобие приведенного, и все это - в 20 миллисекундном апдейте x 100 раз.

 

А потом и получается "не может оно за 5 секунд выполняться, если на моем 16-ядерном пентиуме-666 на 100500Терагерц с терабайтом памяти и плазменной панелью на всю стену в кредит взятой - 2 минуты ! И вообще, кто плазменную панель в кредит не купил - тому надо вообще запретить на форум писать !!!"

 

Короче, замеры - выше, код - выше.

Изменено пользователем Dennis_Chikin
Ссылка на комментарий

local pt = profile_timer()
local sys_ini = system_ini()

pt:start()
for i = 1, 10000 do
sys_ini:r_float("explosion_marks", "dist")
end
pt:stop()
log1("1 ~~~ sys_ini time: "..pt:time())

pt:start()
for i = 1, 10000 do
system_ini():r_float("explosion_marks", "dist")
end
pt:stop()
log1("2 ~~~ system_ini() time: "..pt:time())
В главном меню игры.

[12/02/14 14:03:05] 1 ~~~ sys_ini time: 6490.5009765625
[12/02/14 14:03:05] 2 ~~~ system_ini() time: 17246.419921875

[12/02/14 14:03:20] 1 ~~~ sys_ini time: 5242.6943359375
[12/02/14 14:03:20] 2 ~~~ system_ini() time: 13490.841796875

[12/02/14 14:03:31] 1 ~~~ sys_ini time: 5106.6259765625
[12/02/14 14:03:31] 2 ~~~ system_ini() time: 14303.36328125

[12/02/14 14:03:43] 1 ~~~ sys_ini time: 5217.56640625
[12/02/14 14:03:43] 2 ~~~ system_ini() time: 13807.4453125

[12/02/14 14:04:04] 1 ~~~ sys_ini time: 5126.4877929688
[12/02/14 14:04:04] 2 ~~~ system_ini() time: 18833.763671875

[12/02/14 14:04:51] 1 ~~~ sys_ini time: 5214.3110351563
[12/02/14 14:04:52] 2 ~~~ system_ini() time: 13137.846679688

[12/02/14 14:04:57] 1 ~~~ sys_ini time: 5111.0854492188
[12/02/14 14:04:57] 2 ~~~ system_ini() time: 12934.647460938

[12/02/14 14:04:57] 1 ~~~ sys_ini time: 5214.4775390625
[12/02/14 14:04:57] 2 ~~~ system_ini() time: 13486.434570313
Разница конечно есть, в среднем 5 миллисекунд... Но учитывая, что это всего лишь 1/8 времени апдейта актора, да и читать 10 тыс. параметров вряд ли кто будет - полезность этой оптимизации стремится к нулю.

 

local pt = profile_timer()
local math_random = math.random


pt:start()
for i = 1, 10000 do
math_random(5)
end
pt:stop()
log1("1 ~~~ math_random time: "..pt:time())


pt:start()
for i = 1, 10000 do
math.random(5)
end
pt:stop()
log1("2 ~~~ math.random time: "..pt:time())
Результаты:

 

[12/02/14 14:20:31] 1 ~~~ math_random time: 456.19830322266
[12/02/14 14:20:31] 2 ~~~ math.random time: 1170.6983642578

[12/02/14 14:20:44] 1 ~~~ math_random time: 434.591796875
[12/02/14 14:20:44] 2 ~~~ math.random time: 1108.9953613281

[12/02/14 14:20:55] 1 ~~~ math_random time: 429.08001708984
[12/02/14 14:20:55] 2 ~~~ math.random time: 1104.5678710938

[12/02/14 14:21:25] 1 ~~~ math_random time: 434.51354980469
[12/02/14 14:21:25] 2 ~~~ math.random time: 1108.9071044922

[12/02/14 14:21:27] 1 ~~~ math_random time: 437.62692260742
[12/02/14 14:21:27] 2 ~~~ math.random time: 1111.5510253906
0.7 миллисекунд на 10 тыс. генераций  :D

 

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

Изменено пользователем Shadows
Ссылка на комментарий

, а разве таймер сбрасывается после pt:stop() или pt:start()? Если нет, то разница посчитана неверно, она вообще будет ничтожной :) Если все таки сбрасывается, пардонте.

Сейчас еще раз посмотрел, если вы боретесь за мс, то стОит делать переменные глобальными, как в _g.script, а не как в тестах, хотя что тут говорить, оптимизировать надо явно другие места, это действительно нужно только для рефакторинга.

Изменено пользователем Desertir

ТЧ 1.0004. SAP и Trans mod

github

Ссылка на комментарий

Забавы ради вот вам

local c = get_console()
local log = function(v) c:execute(v) end
local pt = profile_timer()
_G.global_rnd = math.random
local local_rnd = math.random
rnd = math.random

pt:start()
for i = 1, 10000 do
	math.random(5)
end
pt:stop()
log("math.random:"..pt:time())

pt = profile_timer()
pt:start()
for i = 1, 10000 do
	_G.global_rnd(5)
end
pt:stop()
log("_G.global_rnd:"..pt:time())

pt = profile_timer()
pt:start()
for i = 1, 10000 do
	global_rnd(5)
end
pt:stop()
log("global_rnd:"..pt:time())

pt = profile_timer()
pt:start()
for i = 1, 10000 do
	local_rnd(5)
end
pt:stop()
log("local_rnd:"..pt:time())

pt = profile_timer()
pt:start()
for i = 1, 10000 do
	rnd(5)
end
pt:stop()
log("rnd:"..pt:time())

 

 

math.random:    578.04058837891
_G.global_rnd:	600.54803466797
global_rnd:	576.27471923828
local_rnd:	398.26226806641
rnd:		487.28372192383

math.random:	656.1162109375
_G.global_rnd:	597.26733398438
global_rnd:     598.79229736328
local_rnd:	393.96237182617
rnd:		448.24282836914

math.random:	673.52966308594
_G.global_rnd:	606.00970458984
global_rnd:	579.37286376953
local_rnd:	393.57550048828
rnd:		460.28140258789

math.random:	670.90179443359
_G.global_rnd:	661.83203125
global_rnd:	569.04602050781
local_rnd:	423.73452758789
rnd:		426.40435791016

math.random:	610.52740478516
_G.global_rnd:	586.79565429688
global_rnd:	583.48901367188
local_rnd:	405.59097290039
rnd:		459.96835327148

 

 

Видим, что "родное" обращение, как и объявление ее глобальной + обращение через _G или просто по имени в целом одинаковы.

Локальные несколько быстрее, но это все ерунда.

Вот систим_ини

local c = get_console()
local log = function(v) c:execute(v) end
_G.gsi = system_ini()
local si = system_ini()

local pt = profile_timer()
pt:start()
for i = 1, 10000 do
	system_ini():r_float("explosion_marks", "dist")
end
pt:stop()
log("system_ini():"..pt:time())

pt = profile_timer()
pt:start()
for i = 1, 10000 do
	gsi:r_float("explosion_marks", "dist")
end
pt:stop()
log("_G.gsi:"..pt:time())

pt = profile_timer()
pt:start()
for i = 1, 10000 do
	si:r_float("explosion_marks", "dist")
end
pt:stop()
log("si:"..pt:time()) 

 

 

system_ini():	7409.6904296875
_G.gsi:		5028.5361328125
si:		4695.3427734375

system_ini():	8477.212890625
_G.gsi:		4900.01953125
si:		4699.5888671875

system_ini():	7485.7944335938
_G.gsi:		4935.0239257813
si:		4756.3041992188

system_ini():	8623.6474609375
_G.gsi:		4963.302734375
si:		4659.6748046875

system_ini():	7239.9951171875
_G.gsi:		4911.7875976563
si:		4597.4228515625 

 

 

Изменено пользователем Desertir

ТЧ 1.0004. SAP и Trans mod

github

Ссылка на комментарий

В общем, продолжая тему, имеет смысл коснуться печальной истории одной пессимизации, но поскольку касается она в основном Солянки и разнообразных солянкоклонов, то закинул ее в более специализированное место: http://www.amk-team.ru/forum/index.php?showtopic=8830&p=896984

 

У кого такого нет - то просто имейте в виду, что такие грабли есть, а добавлять и заменять ни где ничего не надо. Из солянки же в конечном итоге ВСЕ это - хоть то, что было, хоть предлагаемое на замену, следует убрать. По мере истребления всех имеющихся ссылок на ЭТО.

Ссылка на комментарий

Создайте аккаунт или авторизуйтесь, чтобы оставить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

Создать аккаунт

Зарегистрировать новый аккаунт в нашем сообществе. Это несложно!

Зарегистрировать новый аккаунт

Войти

Есть аккаунт? Войти.

Войти
  • Недавно просматривали   0 пользователей

    • Ни один зарегистрированный пользователь не просматривает эту страницу.
×
×
  • Создать...