@@ -476,35 +476,548 @@ $("#console-command").addEventListener("keydown", (event) => {
// Конфиг сервера
// ---------------------------------------------------------------------------
// Подсказки к самым востребованным параметрам: остальные показываются как есть.
// Подписи и описания параметров servertest.ini. Игра пишет свои комментарии
// на языке сервера, и на Linux они почти всегда английские, поэтому панель
// держит собственные: ключ -> [подпись, описание]. Чего здесь нет, панель
// покажет с описанием из самого конфига.
const CONFIG _HINTS = {
PublicName : "Название сервера в списке серверов" ,
PublicDescription : "Описание сервера" ,
Public : "Показывать сервер в публичном списке (true/false)" ,
Password : "Пароль для входа на сервер (пусто — без пароля)" ,
MaxPlayers : "Максимум игроков" ,
DefaultPort : "Игровой порт (UDP), по умолчанию 16261" ,
UDPPort : "Второй игровой порт, по умолчанию 16262" ,
PVP : "Разрешить бой между игроками" ,
PauseEmpty : "Ставить мир на паузу, когда никого нет онлайн" ,
GlobalChat : "Общий чат для всего сервера" ,
Open : "Пускать игроков без записи в белом списке" ,
ServerWelcomeMessage : "Приветственное сообщение" ,
SaveWorldEveryMinutes : "Как часто сохранять мир, минут" ,
BackupsOnStart : "Делать бэкап силами самой игры при старте" ,
RCONPort : "Порт RCON" ,
RCONPassword : "Пароль RCON" ,
AdminSafehouse : "Разрешить админам входить в чужие убежища" ,
SafetySystem : "Система защиты от PVP-урона" ,
DisplayUserName : "Показывать ники над персонажами" ,
SpawnPoint : "Точка появления новых игроков (x,y,z)" ,
Mods : "Список модов — редактируется во вкладке «Моды»" ,
WorkshopItems : "Моды мастерской — редактируется во вкладке «Моды»" ,
Map : "Карты — редактируется во вкладке «Моды»" ,
PublicName : [ "Название сервера" , "Показывается в списке серверов игры и Steam." ] ,
PublicDescription : [ "Описание сервера" , "Текст под названием в списке серверов. Перенос строки — <LINE>." ] ,
Public : [ "Показывать в списке серверов" , "Сервер со Steam-авторизацией виден в браузере Steam в любом случае." ] ,
Open : [ "Пускать без белого списка" , "Если выключено, учётные записи создаёт администратор вручную." ] ,
MaxPlayers : [ "Максимум игроков" , "Администраторы не считаются. Выше 32 игроков возможны просадки." ] ,
ServerWelcomeMessage : [ "Приветственное сообщение" , "Показывается в чате сразу после входа. Поддерживает <LINE> и цвета <RGB:r,g,b>." ] ,
ResetID : [ "Идентификатор сброса" , "Меняется при мягком сбросе мира: клиенты с другим значением создают нового персонажа." ] ,
ServerPlayerID : [ "Идентификатор персонажей" , "По нему игра отличает персонажей этого сервера от чужих." ] ,
Seed : [ "Зерно генерации мира" , "Чтобы сменить, впишите новое значение и удалите map_worldgen.bin в папке мира." ] ,
server _browser _announced _ip : [ "Объявляемый IP" , "Нужен, когда у машины несколько адресов и в список серверов должен попасть определённый." ] ,
Password : [ "Пароль сервера" , "Пусто — вход без пароля." ] ,
MaxAccountsPerUser : [ "Учёток на игрока" , "Сколько персонажей может завести один Steam-аккаунт. 0 — без ограничения." ] ,
DenyLoginOnOverloadedServer : [ "Не пускать при перегрузке" , "Отказывать во входе, когда сервер не справляется с нагрузкой." ] ,
DropOffWhiteListAfterDeath : [ "Убирать из белого списка после смерти" , "Погибший игрок теряет доступ и должен получить его заново." ] ,
LoginQueueEnabled : [ "Очередь на вход" , "Лишние игроки ждут освободившегося места вместо отказа." ] ,
LoginQueueConnectTimeout : [ "Таймаут очереди, сек" , "Сколько ждать подключения игрока, дошедшего до начала очереди." ] ,
AllowCoop : [ "Разрешить разделённый экран" , "Несколько игроков с одного клиента." ] ,
AllowNonAsciiUsername : [ "Ники не только латиницей" , "Разрешить кириллицу и другие не-ASCII символы в никах." ] ,
SteamScoreboard : [ "Показывать Steam-имена" , "Отображать имена и аватары Steam в списке игроков." ] ,
SteamVAC : [ "Защита Steam VAC" , "Включает античит-защиту Valve." ] ,
DoLuaChecksum : [ "Сверять контрольные суммы Lua" , "Отключать игроков с изменёнными Lua-файлами." ] ,
DefaultPort : [ "Игровой порт (UDP)" , "По умолчанию 16261. Должен быть открыт на firewall." ] ,
UDPPort : [ "Второй игровой порт" , "По умолчанию 16262. Тоже должен быть открыт." ] ,
RCONPort : [ "Порт RCON" , "Порт для внешнего управления консолью сервера." ] ,
RCONPassword : [ "Пароль RCON" , "Пусто — RCON выключен. Панели он не нужен: она пишет прямо в консоль." ] ,
PingLimit : [ "Предел пинга, мс" , "Игроков с задержкой выше выкидывает. Ниже 100 ставить не стоит." ] ,
UPnP : [ "Пробрасывать порты через UPnP" , "Автоматически открыть порты на роутере, если тот поддерживает UPnP." ] ,
MaxPacketsPerSecond : [ "Пакетов в секунду" , "Ограничение сетевого потока на игрока." ] ,
ClientCommandFilter : [ "Фильтр команд клиента" , "Список команд, которые не пишутся в журнал." ] ,
MultiplayerStatisticsPeriod : [ "Период сбора статистики" , "Как часто писать сетевую статистику. 0 — не собирать." ] ,
PVP : [ "Разрешить бой между игроками" , "Игроки могут ранить и убивать друг друга." ] ,
PVPLogToolChat : [ "Писать PVP в чат админов" , "Сообщать администраторам о PVP-стычках в чате." ] ,
PVPLogToolFile : [ "Писать PVP в файл" , "Вести журнал PVP-стычек на диске." ] ,
SafetySystem : [ "Система безопасности" , "Урон игроку проходит, только когда оба сняли защиту." ] ,
ShowSafety : [ "Показывать значок защиты" , "Иконка над головой показывает, снята ли защита." ] ,
SafetyToggleTimer : [ "Задержка переключения, сек" , "Сколько секунд занимает снятие или включение защиты." ] ,
SafetyCooldownTimer : [ "Откат переключения, сек" , "Через сколько секунд защиту можно переключить снова." ] ,
SafetyDisconnectDelay : [ "Задержка выхода в PVP, сек" , "Сколько игрок остаётся в мире после отключения во время боя." ] ,
PVPMeleeDamageModifier : [ "Урон в ближнем бою, %" , "Множитель урона игрок-игрок для оружия ближнего боя." ] ,
PVPFirearmDamageModifier : [ "Урон от огнестрела, %" , "Множитель урона игрок-игрок для огнестрельного оружия." ] ,
PVPMeleeWhileHitReaction : [ "Удары во время реакции на попадание" , "Разрешить бить игрока, пока тот отшатывается от предыдущего удара." ] ,
KnockedDownAllowed : [ "Разрешить сбивать с ног" , "Игроки могут сбивать друг друга с ног." ] ,
SneakModeHideFromOtherPlayers : [ "Скрытность прячет от игроков" , "В режиме скрытности игрока не видят другие игроки." ] ,
PlayerBumpPlayer : [ "Толкать других игроков" , "Столкновения игроков между собой." ] ,
UsePhysicsHitReaction : [ "Физическая реакция на удар" , "Включает физику отдачи при попадании." ] ,
CarEngineAttractionModifier : [ "Шум двигателя для зомби" , "Насколько сильно двигатель привлекает зомби." ] ,
Map : [ "Карты" , "Список карт через точку с запятой. Первой идёт карта мода, последней — базовая." ] ,
SpawnPoint : [ "Точка появления" , "Координаты x,y,z. Нули — стандартные точки появления карты." ] ,
SpawnItems : [ "Стартовые предметы" , "Список предметов, которые получает новый персонаж." ] ,
SaveWorldEveryMinutes : [ "Сохранять мир каждые, мин" , "0 — сохранять только при выходе игроков и остановке сервера." ] ,
PauseEmpty : [ "Пауза без игроков" , "Игровое время останавливается, когда на сервере никого нет." ] ,
NoFire : [ "Запретить огонь" , "Полностью отключает распространение огня." ] ,
AllowDestructionBySledgehammer : [ "Разрешить кувалду" , "Игроки могут ломать стены и объекты кувалдой." ] ,
SledgehammerOnlyInSafehouse : [ "Кувалда только в убежище" , "Ломать можно лишь внутри своего убежища." ] ,
PlayerRespawnWithSelf : [ "Возрождение у своего трупа" , "Разрешить появляться на месте гибели прошлого персонажа." ] ,
PlayerRespawnWithOther : [ "Возрождение у товарища" , "Разрешить появляться рядом с другим игроком." ] ,
FastForwardMultiplier : [ "Ускорение времени" , "Во сколько раз ускоряется время при перемотке." ] ,
RemovePlayerCorpsesOnCorpseRemoval : [ "Убирать и трупы игроков" , "Автоочистка трупов затрагивает и погибших игроков." ] ,
TrashDeleteAll : [ "Кнопка «удалить всё»" , "Разрешить очищать контейнер целиком одним действием." ] ,
BloodSplatLifespanDays : [ "Кровь исчезает через, дней" , "0 — пятна крови остаются навсегда." ] ,
ItemNumbersLimitPerContainer : [ "Предметов в контейнере" , "Ограничение на количество предметов. 0 — без ограничения." ] ,
SafehousePreventsLootRespawn : [ "Убежище блокирует респавн лута" , "В зданиях-убежищах лут заново не появляется." ] ,
SwitchZombiesOwnershipEachUpdate : [ "Передавать зомби каждый такт" , "Меняет схему распределения зомби между клиентами." ] ,
UltraSpeedDoesnotAffectToAnimals : [ "Перемотка не ускоряет животных" , "На максимальной скорости времени животные живут в обычном темпе." ] ,
SleepAllowed : [ "Разрешить сон" , "Игроки могут спать; на сервере с несколькими игроками время не перематывается." ] ,
SleepNeeded : [ "Требовать сон" , "Усталость накапливается и требует сна." ] ,
SpeedLimit : [ "Ограничение скорости" , "Предел скорости транспорта, км/ч." ] ,
BackupsCount : [ "Сколько копий хранить" , "Резервные копии, которые делает сама игра." ] ,
BackupsOnStart : [ "Копия при запуске" , "Игра делает бэкап мира при каждом старте сервера." ] ,
BackupsOnVersionChange : [ "Копия при смене версии" , "Бэкап перед первым запуском на новой версии игры." ] ,
BackupsPeriod : [ "Период копий, мин" , "0 — периодические бэкапы игры выключены. У панели есть свои." ] ,
Mods : [ "Включённые моды" , "Список Mod ID в порядке загрузки. Правится на вкладке «Моды»." ] ,
WorkshopItems : [ "Загружаемые из мастерской" , "Список Workshop ID. Правится на вкладке «Моды»." ] ,
GlobalChat : [ "Общий чат" , "Сообщения видят все игроки на сервере." ] ,
ChatStreams : [ "Доступные каналы чата" , "Список каналов: s — сказать, r — рация, a — админ, w — крик и так далее." ] ,
VoiceEnable : [ "Голосовой чат" , "Разрешить голосовое общение." ] ,
VoiceMinDistance : [ "Слышимость вблизи" , "Расстояние, на котором голос слышен без затухания." ] ,
VoiceMaxDistance : [ "Предел слышимости" , "Расстояние, дальше которого голос не слышен." ] ,
Voice3D : [ "Объёмный звук голоса" , "Голос слышен с направления, откуда говорит игрок." ] ,
ChatMessageCharacterLimit : [ "Длина сообщения" , "Максимум символов в одном сообщении чата." ] ,
ChatMessageSlowModeTime : [ "Пауза между сообщениями, сек" , "Защита от флуда: минимальный интервал между сообщениями." ] ,
AnnounceDeath : [ "Объявлять о смертях" , "Сообщать в чат о гибели игроков." ] ,
AnnounceAnimalDeath : [ "Объявлять о гибели животных" , "Сообщать в чат, когда погибает домашнее животное." ] ,
BanKickGlobalSound : [ "Звук при бане и кике" , "Проигрывать звук всем при исключении игрока." ] ,
BadWordListFile : [ "Файл со списком брани" , "Путь к списку запрещённых слов." ] ,
GoodWordListFile : [ "Файл с исключениями" , "Слова, которые фильтр пропускает." ] ,
BadWordPolicy : [ "Что делать с бранью" , "Заменять слово или блокировать сообщение целиком." ] ,
BadWordReplacement : [ "Чем заменять" , "Текст, на который меняется запрещённое слово." ] ,
DisplayUserName : [ "Ники над головами" , "Показывать имена игроков в мире." ] ,
ShowFirstAndLastName : [ "Имя и фамилия" , "Показывать полное имя персонажа вместо ника." ] ,
UsernameDisguises : [ "Маскировка ника" , "Ник скрывается, когда лицо игрока закрыто маской или шлемом." ] ,
HideDisguisedUserName : [ "Прятать ник замаскированных" , "У замаскированного игрока имя не показывается вовсе." ] ,
MouseOverToSeeDisplayName : [ "Имя по наведению" , "Ник виден, только если навести на игрока курсор." ] ,
HidePlayersBehindYou : [ "Скрывать игроков за спиной" , "Не показывать тех, кто вне поля зрения персонажа." ] ,
MapRemotePlayerVisibility : [ "Игроки на карте" , "Показывать ли других игроков на внутриигровой карте." ] ,
ShowCoordinates : [ "Показывать координаты" , "Координаты видны игрокам в интерфейсе." ] ,
DisableScoreboard : [ "Отключить таблицу игроков" , "Скрыть список игроков сервера." ] ,
HideAdminsInPlayerList : [ "Прятать администраторов" , "Администраторы не видны в списке игроков." ] ,
PlayerSafehouse : [ "Игроки могут ставить убежища" , "Разрешить обычным игрокам объявлять здание своим." ] ,
AdminSafehouse : [ "Убежища только у админов" , "Объявлять убежища могут лишь администраторы." ] ,
SafehouseAllowTrepass : [ "Пускать посторонних" , "Разрешить входить в чужое убежище." ] ,
SafehouseAllowFire : [ "Огонь в убежище" , "Разрешить возгорания на территории убежища." ] ,
SafehouseAllowLoot : [ "Брать вещи в убежище" , "Разрешить посторонним забирать предметы из убежища." ] ,
SafehouseAllowRespawn : [ "Возрождение в убежище" , "Члены убежища появляются в нём после гибели." ] ,
SafehouseDaySurvivedToClaim : [ "Дней до права на убежище" , "Сколько дней надо прожить, прежде чем объявлять здание своим." ] ,
SafeHouseRemovalTime : [ "Освобождение убежища, ч" , "Через сколько часов отсутствия владельца убежище снимается." ] ,
SafehouseAllowNonResidential : [ "Нежилые здания под убежище" , "Разрешить занимать магазины, склады и прочие нежилые постройки." ] ,
SafehouseDisableDisguises : [ "Без маскировки в убежище" , "Маскировка не действует на территории убежища." ] ,
MaxSafezoneSize : [ "Размер безопасной зоны" , "Максимальная площадь убежища в клетках." ] ,
DisableSafehouseWhenOwnerConnected : [ "Снимать защиту при владельце" , "Пока владелец в игре, защита убежища не действует." ] ,
DisableSafehouseWhenPlayerConnected : [ "Снимать защиту при игроках" , "Защита не действует, пока внутри есть игроки." ] ,
Faction : [ "Разрешить фракции" , "Игроки могут создавать группировки." ] ,
FactionDaySurvivedToCreate : [ "Дней до создания фракции" , "Сколько дней нужно прожить, чтобы основать фракцию." ] ,
FactionPlayersRequiredForTag : [ "Игроков для метки фракции" , "Сколько участников нужно, чтобы у фракции появился тег." ] ,
War : [ "Разрешить войны фракций" , "Фракции могут объявлять друг другу войну." ] ,
WarStartDelay : [ "Задержка начала войны" , "Через сколько времени после объявления начинается война." ] ,
WarDuration : [ "Длительность войны" , "Сколько длится объявленная война." ] ,
WarSafehouseHitPoints : [ "Прочность убежищ в войне" , "Сколько урона выдерживают постройки во время войны." ] ,
DisableRadioStaff : [ "Скрыть рацию персонала" , "Отключить служебный радиоканал." ] ,
DisableRadioAdmin : [ "Скрыть рацию админов" , "Отключить радиоканал администраторов." ] ,
DisableRadioGM : [ "Скрыть рацию гейм-мастеров" , "Отключить радиоканал GM." ] ,
DisableRadioOverseer : [ "Скрыть рацию наблюдателей" , "Отключить радиоканал наблюдателей." ] ,
DisableRadioModerator : [ "Скрыть рацию модераторов" , "Отключить радиоканал модераторов." ] ,
DisableRadioInvisible : [ "Скрыть рацию невидимых" , "Отключить радиоканал скрытых персонажей." ] ,
DiscordEnable : [ "Связь с Discord" , "Пересылать чат сервера в Discord и обратно." ] ,
DiscordToken : [ "Токен бота" , "Токен Discord-бота, через которого идёт связь." ] ,
DiscordChannel : [ "Канал чата" , "Название канала для игрового чата." ] ,
DiscordChannelID : [ "Идентификатор канала" , "Числовой ID канала Discord." ] ,
DiscordChatChannel : [ "Канал чата" , "Канал, куда пересылается игровой чат." ] ,
DiscordLogChannel : [ "Канал журнала" , "Канал для служебных сообщений сервера." ] ,
DiscordCommandChannel : [ "Канал команд" , "Канал, из которого принимаются команды." ] ,
WebhookAddress : [ "Адрес вебхука" , "Куда отправлять события сервера по HTTP." ] ,
AntiCheatSafety : [ "Античит: система защиты" , "Проверка попыток обойти защиту от урона." ] ,
AntiCheatSpeed : [ "Античит: скорость" , "Проверка неестественно быстрого перемещения." ] ,
AntiCheatNoClip : [ "Античит: проход сквозь стены" , "Проверка перемещения через препятствия." ] ,
AntiCheatHit : [ "Античит: попадания" , "Проверка ударов с недостижимого расстояния." ] ,
AntiCheatPacketException : [ "Античит: битые пакеты" , "Реакция на некорректные сетевые пакеты." ] ,
AntiCheatPermission : [ "Античит: права" , "Проверка попыток выполнить действия без прав." ] ,
AntiCheatXP : [ "Античит: опыт" , "Проверка неестественного роста навыков." ] ,
AntiCheatSafeHouse : [ "Античит: убежища" , "Проверка обхода защиты убежищ." ] ,
AntiCheatPlayer : [ "Античит: персонаж" , "Проверка изменённых характеристик персонажа." ] ,
AntiCheatChecksum : [ "Античит: контрольные суммы" , "Сверка файлов игры у клиента." ] ,
ClientActionLogs : [ "Журнал действий клиента" , "Какие действия игроков записывать в журнал." ] ,
PerkLogs : [ "Журнал навыков" , "Записывать изменения уровней навыков." ] ,
DisableVehicleTowing : [ "Запретить буксировку" , "Машины нельзя цеплять друг к другу." ] ,
DisableTrailerTowing : [ "Запретить прицепы" , "Прицепы нельзя цеплять к машинам." ] ,
DisableBurntTowing : [ "Не буксировать сгоревшие" , "Сгоревший транспорт нельзя буксировать." ] ,
AutoCreateUserInWhiteList : [ "Заводить учётки автоматически" , "Игрок при первом входе сам создаёт логин и пароль." ] ,
ConstructionPreventsLootRespawn : [ "Постройки блокируют респавн лута" , "В зданиях, где игроки что-то построили, лут заново не появляется." ] ,
HoursForLootRespawn : [ "Респавн лута через, часов" , "0 — лут не появляется заново." ] ,
MaxItemsForLootRespawn : [ "Предметов при респавне" , "Сколько предметов возвращается в контейнер при обновлении лута." ] ,
KickFastPlayers : [ "Выкидывать за скорость" , "Отключать игроков, движущихся неестественно быстро. Может срабатывать ложно." ] ,
MinutesPerPage : [ "Минут на страницу" , "Сколько игрового времени уходит на чтение одной страницы книги." ] ,
} ;
const BOOLEAN _VALUES = new Set ( [ "true" , "false" ] ) ;
// Категории параметров servertest.ini. Порядок важен: в нём и рисуются
// разделы, а всё, чего здесь нет, попадает в «Прочее» — конфиг PZ меняется от
// версии к версии, и новые ключи не должны пропадать со страницы.
const CONFIG _GROUPS = [
{
title : "Основное" ,
keys : [ "PublicName" , "PublicDescription" , "Public" , "Open" , "MaxPlayers" ,
"ServerWelcomeMessage" , "ResetID" , "ServerPlayerID" , "Seed" ,
"server_browser_announced_ip" , "PublicServerIP" , "ServerBrowserAnnouncedIP" ] ,
} ,
{
title : "Доступ и учётные записи" ,
keys : [ "Password" , "MaxAccountsPerUser" , "DenyLoginOnOverloadedServer" ,
"AutoCreateUserInWhiteList" , "DropOffWhiteListAfterDeath" , "LoginQueueEnabled" ,
"LoginQueueConnectTimeout" , "AllowCoop" , "AllowNonAsciiUsername" ,
"SteamScoreboard" , "SteamVAC" , "DoLuaChecksum" ] ,
} ,
{
title : "Сеть и порты" ,
keys : [ "DefaultPort" , "UDPPort" , "RCONPort" , "RCONPassword" , "PingLimit" , "UPnP" ,
"MaxPacketsPerSecond" , "ClientCommandFilter" , "MultiplayerStatisticsPeriod" ] ,
} ,
{
title : "PVP и урон" ,
keys : [ "PVP" , "PVPLogToolChat" , "PVPLogToolFile" , "SafetySystem" , "ShowSafety" ,
"SafetyToggleTimer" , "SafetyCooldownTimer" , "SafetyDisconnectDelay" ,
"PVPMeleeDamageModifier" , "PVPFirearmDamageModifier" , "PVPMeleeWhileHitReaction" ,
"KnockedDownAllowed" , "SneakModeHideFromOtherPlayers" , "PlayerBumpPlayer" ,
"UsePhysicsHitReaction" , "CarEngineAttractionModifier" ] ,
} ,
{
title : "Мир и правила" ,
keys : [ "Map" , "SpawnPoint" , "SpawnItems" , "SaveWorldEveryMinutes" , "PauseEmpty" ,
"NoFire" , "AllowDestructionBySledgehammer" , "SledgehammerOnlyInSafehouse" ,
"PlayerRespawnWithSelf" , "PlayerRespawnWithOther" , "FastForwardMultiplier" ,
"RemovePlayerCorpsesOnCorpseRemoval" , "TrashDeleteAll" , "BloodSplatLifespanDays" ,
"ItemNumbersLimitPerContainer" , "SafehousePreventsLootRespawn" ,
"ConstructionPreventsLootRespawn" , "HoursForLootRespawn" , "MaxItemsForLootRespawn" ,
"SwitchZombiesOwnershipEachUpdate" , "UltraSpeedDoesnotAffectToAnimals" ,
"SleepAllowed" , "SleepNeeded" , "SpeedLimit" ] ,
} ,
{
title : "Бэкапы самой игры" ,
keys : [ "BackupsCount" , "BackupsOnStart" , "BackupsOnVersionChange" , "BackupsPeriod" ] ,
} ,
{
title : "Моды" ,
keys : [ "Mods" , "WorkshopItems" ] ,
} ,
{
title : "Чат и голос" ,
keys : [ "GlobalChat" , "ChatStreams" , "VoiceEnable" , "VoiceMinDistance" , "VoiceMaxDistance" ,
"Voice3D" , "ChatMessageCharacterLimit" , "ChatMessageSlowModeTime" , "AnnounceDeath" ,
"AnnounceAnimalDeath" , "MinutesPerPage" , "BanKickGlobalSound" ] ,
} ,
{
title : "Фильтр брани" ,
keys : [ "BadWordListFile" , "GoodWordListFile" , "BadWordPolicy" , "BadWordReplacement" ] ,
} ,
{
title : "Видимость игроков" ,
keys : [ "DisplayUserName" , "ShowFirstAndLastName" , "UsernameDisguises" ,
"HideDisguisedUserName" , "MouseOverToSeeDisplayName" , "HidePlayersBehindYou" ,
"MapRemotePlayerVisibility" , "ShowCoordinates" , "DisableScoreboard" ,
"HideAdminsInPlayerList" ] ,
} ,
{
title : "Убежища" ,
keys : [ "PlayerSafehouse" , "AdminSafehouse" , "SafehouseAllowTrepass" , "SafehouseAllowFire" ,
"SafehouseAllowLoot" , "SafehouseAllowRespawn" , "SafehouseDaySurvivedToClaim" ,
"SafeHouseRemovalTime" , "SafehouseAllowNonResidential" , "SafehouseDisableDisguises" ,
"MaxSafezoneSize" , "DisableSafehouseWhenOwnerConnected" ,
"DisableSafehouseWhenPlayerConnected" ] ,
} ,
{
title : "Фракции и войны" ,
keys : [ "Faction" , "FactionDaySurvivedToCreate" , "FactionPlayersRequiredForTag" ,
"War" , "WarStartDelay" , "WarDuration" , "WarSafehouseHitPoints" ] ,
} ,
{
title : "Рации" ,
keys : [ "DisableRadioStaff" , "DisableRadioAdmin" , "DisableRadioGM" , "DisableRadioOverseer" ,
"DisableRadioModerator" , "DisableRadioInvisible" ] ,
} ,
{
title : "Discord" ,
keys : [ "DiscordEnable" , "DiscordToken" , "DiscordChannel" , "DiscordChannelID" ,
"DiscordChatChannel" , "DiscordLogChannel" , "DiscordCommandChannel" , "WebhookAddress" ] ,
} ,
{
title : "Античит и журналы" ,
keys : [ "AntiCheatSafety" , "AntiCheatSpeed" , "AntiCheatNoClip" , "AntiCheatHit" ,
"AntiCheatPacketException" , "AntiCheatPermission" , "AntiCheatXP" , "AntiCheatSafeHouse" ,
"AntiCheatPlayer" , "AntiCheatChecksum" , "KickFastPlayers" , "ClientActionLogs" , "PerkLogs" ] ,
// Нумерованных проверок два десятка, перечислять их поимённо бессмысленно.
prefixes : [ "AntiCheatProtectionType" ] ,
} ,
{
title : "Транспорт" ,
keys : [ "DisableVehicleTowing" , "DisableTrailerTowing" , "DisableBurntTowing" ] ,
} ,
] ;
// Подписи параметров песочницы. Подробное описание игра пишет сама (в Build 42
// с расшифровкой каждого значения), но на языке сервера — обычно английском,
// поэтому подпись даём свою, а описание берём из файла.
const SANDBOX _HINTS = {
Zombies : [ "Количество зомби" ] ,
Distribution : [ "Распределение зомби" ] ,
ZombiesRespawn : [ "Восстановление популяции" ] ,
ZombieRespawn : [ "Восстановление популяции" ] ,
ZombieMigrate : [ "Миграция зомби" ] ,
ZombieVoronoiNoise : [ "Неравномерность расселения" ] ,
ZombieAttractionMultiplier : [ "Притяжение на шум" ] ,
ZombieHealthImpact : [ "Здоровье зомби" ] ,
ZombiePopLootEffect : [ "Влияние популяции на лут" ] ,
MultiHitZombies : [ "Удар по нескольким зомби" ] ,
RearVulnerability : [ "Уязвимость со спины" ] ,
SirenEffectsZombies : [ "Сирены привлекают зомби" ] ,
RallyGroupSize : [ "Размер скопления" ] ,
RallyTravelDistance : [ "Дальность схода в группу" ] ,
RallyGroupSeparation : [ "Расстояние между группами" ] ,
RallyGroupRadius : [ "Плотность группы" ] ,
HoursForCorpseRemoval : [ "Трупы исчезают через, часов" ] ,
DecayingCorpseHealthImpact : [ "Вред от разлагающихся трупов" ] ,
BloodLevel : [ "Количество крови" ] ,
BloodSplatLifespanDays : [ "Кровь исчезает через, дней" ] ,
DayLength : [ "Длина игрового дня" ] ,
StartYear : [ "Год начала" ] ,
StartMonth : [ "Месяц начала" ] ,
StartDay : [ "День начала" ] ,
StartTime : [ "Время начала" ] ,
DayNightCycle : [ "Смена дня и ночи" ] ,
NightLength : [ "Длина ночи" ] ,
NightDarkness : [ "Темнота ночью" ] ,
TimeSinceApo : [ "Времени с начала эпидемии" ] ,
WaterShut : [ "Отключение воды" ] ,
ElecShut : [ "Отключение электричества" ] ,
WaterShutModifier : [ "Дней до отключения воды" ] ,
ElecShutModifier : [ "Дней до отключения света" ] ,
Alarm : [ "Частота сигнализаций" ] ,
AlarmDecay : [ "Затухание сигнализаций" ] ,
AlarmDecayModifier : [ "Скорость затухания сигнализаций" ] ,
LockedHouses : [ "Запертые дома" ] ,
ErosionSpeed : [ "Скорость зарастания" ] ,
ErosionDays : [ "Дней до полного зарастания" ] ,
EndRegen : [ "Восстановление выносливости" ] ,
Helicopter : [ "Вертолёт" ] ,
MetaEvent : [ "Случайные события" ] ,
SleepingEvent : [ "Ночные события" ] ,
AnnotatedMapChance : [ "Шанс найти карту с пометками" ] ,
VehicleStoryChance : [ "Частота историй с транспортом" ] ,
ZoneStoryChance : [ "Частота историй на местности" ] ,
SurvivorHouseChance : [ "Частота домов выживших" ] ,
MaximumLootedBuildingRooms : [ "Комнат в разграбленном здании" ] ,
ClimateCycle : [ "Смена времён года" ] ,
FogCycle : [ "Частота туманов" ] ,
Temperature : [ "Температура" ] ,
TemperatureModifier : [ "Поправка температуры" ] ,
Rain : [ "Дожди" ] ,
RainModifier : [ "Поправка дождей" ] ,
MaxFogIntensity : [ "Плотность тумана" ] ,
MaxRainFxIntensity : [ "Сила дождя" ] ,
EnableSnowOnGround : [ "Снег на земле" ] ,
LootRespawn : [ "Обновление лута" ] ,
SeenHoursPreventLootRespawn : [ "Часов после осмотра до респавна" ] ,
HoursForLootRespawn : [ "Респавн лута через, часов" ] ,
MaxItemsForLootRespawn : [ "Предметов при респавне" ] ,
ConstructionPreventsLootRespawn : [ "Постройки блокируют респавн" ] ,
RollsMultiplier : [ "Множитель бросков лута" ] ,
FoodLoot : [ "Еда" ] ,
CannedFoodLoot : [ "Консервы" ] ,
LiteratureLoot : [ "Книги и журналы" ] ,
SurvivalGearsLoot : [ "Снаряжение" ] ,
MedicalLoot : [ "Медикаменты" ] ,
WeaponLoot : [ "Оружие" ] ,
RangedWeaponLoot : [ "Огнестрельное оружие" ] ,
AmmoLoot : [ "Патроны" ] ,
MechanicsLoot : [ "Автозапчасти" ] ,
OtherLoot : [ "Прочее" ] ,
SkillBookLoot : [ "Учебники навыков" ] ,
RecipeResourceLoot : [ "Материалы для рецептов" ] ,
RemoveStoryLoot : [ "Убирать сюжетный лут" ] ,
RemoveZombieLoot : [ "Убирать лут с зомби" ] ,
MaximumLooted : [ "Предел разграбленности" ] ,
DaysUntilMaximumLooted : [ "Дней до предела разграбленности" ] ,
RuralLooted : [ "Разграбленность сельской местности" ] ,
MaximumDiminishedLoot : [ "Предел оскудения лута" ] ,
DaysUntilMaximumDiminishedLoot : [ "Дней до оскудения лута" ] ,
XpMultiplier : [ "Множитель опыта" ] ,
XpMultiplierAffectsPassive : [ "Опыт и пассивные навыки" ] ,
StatsDecrease : [ "Скорость падения характеристик" ] ,
Endurance : [ "Выносливость" ] ,
Nutrition : [ "Учитывать питание" ] ,
StarterKit : [ "Стартовый набор" ] ,
InjurySeverity : [ "Тяжесть травм" ] ,
BoneFracture : [ "Переломы" ] ,
CharacterFreePoints : [ "Свободных очков персонажа" ] ,
ConstructionBonusPoints : [ "Бонус к строительству" ] ,
NightVisionEnabled : [ "Ночное зрение" ] ,
InjuredHealingModifier : [ "Скорость заживления" ] ,
MuscleStrainFactor : [ "Растяжение мышц" ] ,
DiscomfortFactor : [ "Дискомфорт" ] ,
WoundInfectionFactor : [ "Заражение ран" ] ,
NegativeTraitsPenalty : [ "Штраф отрицательных черт" ] ,
EnablePoisoning : [ "Отравления" ] ,
ClothingDegradation : [ "Износ одежды" ] ,
AllClothesUnlocked : [ "Вся одежда доступна" ] ,
NoBlackClothes : [ "Без чёрной одежды" ] ,
EasyClimbing : [ "Лёгкое перелезание" ] ,
AttackBlockMovements : [ "Атака блокирует движение" ] ,
MetaKnowledge : [ "Знание рецептов заранее" ] ,
SeeNotLearntRecipe : [ "Видеть неизученные рецепты" ] ,
LiteratureCooldown : [ "Перерыв между чтением" ] ,
MinutesPerPage : [ "Минут на страницу" ] ,
LevelForMediaXPCutoff : [ "Предел опыта с кассет" ] ,
LevelForDismantleXPCutoff : [ "Предел опыта с разборки" ] ,
FoodRotSpeed : [ "Скорость порчи еды" ] ,
FridgeFactor : [ "Эффективность холодильников" ] ,
MaggotSpawn : [ "Появление личинок" ] ,
EnableTaintedWaterText : [ "Помечать грязную воду" ] ,
FishAbundance : [ "Обилие рыбы" ] ,
NatureAbundance : [ "Обилие даров природы" ] ,
PlantResilience : [ "Живучесть растений" ] ,
PlantAbundance : [ "Урожайность" ] ,
Farming : [ "Скорость роста" ] ,
FarmingSpeedNew : [ "Скорость земледелия" ] ,
FarmingAmountNew : [ "Урожай с грядки" ] ,
CompostTime : [ "Время компостирования" ] ,
KillInsideCrops : [ "Губить посевы в помещении" ] ,
PlantGrowingSeasons : [ "Сезонность посевов" ] ,
PlaceDirtAboveground : [ "Грядки на любом уровне" ] ,
ClayLakeChance : [ "Глина у озёр" ] ,
ClayRiverChance : [ "Глина у рек" ] ,
DaysForRottenFoodRemoval : [ "Дней до удаления гнили" ] ,
AidPackage : [ "Сброс гуманитарной помощи" ] ,
AnimalStatsModifier : [ "Характеристики животных" ] ,
AnimalMetaStatsModifier : [ "Дикие животные" ] ,
AnimalPregnancyTime : [ "Срок беременности" ] ,
AnimalAgeModifier : [ "Скорость взросления" ] ,
AnimalMilkIncModifier : [ "Надой молока" ] ,
AnimalWoolIncModifier : [ "Настриг шерсти" ] ,
AnimalRanchChance : [ "Частота ферм" ] ,
AnimalGrassRegrowTime : [ "Отрастание травы" ] ,
AnimalMetaPredator : [ "Хищники" ] ,
AnimalMatingSeason : [ "Сезон спаривания" ] ,
AnimalEggHatch : [ "Вылупление яиц" ] ,
AnimalSoundAttractZombies : [ "Животные привлекают зомби" ] ,
AnimalTrackChance : [ "Частота следов" ] ,
AnimalPathChance : [ "Частота троп" ] ,
MaximumRatIndex : [ "Предел числа крыс" ] ,
DaysUntilMaximumRatIndex : [ "Дней до предела крыс" ] ,
CarSpawnRate : [ "Частота машин" ] ,
ChanceHasGas : [ "Шанс бензина в баке" ] ,
InitialGas : [ "Бензина в баке" ] ,
CarGasConsumption : [ "Расход топлива" ] ,
LockedCar : [ "Запертые машины" ] ,
CarGeneralCondition : [ "Состояние машин" ] ,
CarDamageOnImpact : [ "Урон машине при ударе" ] ,
DamageToPlayerFromHitByACar : [ "Урон игроку от наезда" ] ,
TrafficJam : [ "Пробки на дорогах" ] ,
CarAlarm : [ "Автосигнализации" ] ,
PlayerDamageFromCrash : [ "Урон при аварии" ] ,
SirenShutoffHours : [ "Сирена смолкает через, часов" ] ,
RecentlySurvivorVehicles : [ "Машины выживших" ] ,
EnableVehicles : [ "Транспорт в игре" ] ,
VehicleEasyUse : [ "Упрощённое управление" ] ,
MassInvasion : [ "Массовое нашествие" ] ,
FuelStationGas : [ "Топливо на заправках" ] ,
FuelStationGasInfinite : [ "Бесконечное топливо" ] ,
FuelStationGasMin : [ "Минимум топлива на заправке" ] ,
FuelStationGasMax : [ "Максимум топлива на заправке" ] ,
FuelStationGasEmptyChance : [ "Шанс пустой заправки" ] ,
FirearmUseDamageChance : [ "Износ при стрельбе" ] ,
FirearmNoiseMultiplier : [ "Громкость выстрелов" ] ,
FirearmJamMultiplier : [ "Частота осечек" ] ,
FirearmMoodleMultiplier : [ "Влияние состояния на меткость" ] ,
FirearmWeatherMultiplier : [ "Влияние погоды на меткость" ] ,
FirearmHeadGearEffect : [ "Влияние головного убора" ] ,
Generator : [ "Расход топлива генератора" ] ,
GeneratorSpawning : [ "Частота генераторов" ] ,
GeneratorFuelConsumption : [ "Расход генератора" ] ,
GeneratorTileRange : [ "Радиус питания генератора" ] ,
GeneratorVerticalPowerRange : [ "Этажей питания генератора" ] ,
AllowExteriorGenerator : [ "Генератор на улице" ] ,
FireSpread : [ "Распространение огня" ] ,
MaximumFireFuelHours : [ "Часов горения топлива" ] ,
LightBulbLifespan : [ "Срок службы лампочек" ] ,
VERSION : [ "Версия набора настроек" ] ,
} ;
// Категории верхнеуровневых параметров песочницы. Вложенные (ZombieLore.Speed
// и подобные) группируются сами — по имени таблицы, в которой лежат.
const SANDBOX _GROUPS = [
{
title : "Зомби" ,
keys : [ "Zombies" , "Distribution" , "ZombiesRespawn" , "ZombieRespawn" , "ZombieMigrate" ,
"ZombieVoronoiNoise" , "ZombieAttractionMultiplier" , "ZombieHealthImpact" ,
"ZombiePopLootEffect" , "MultiHitZombies" , "RearVulnerability" , "SirenEffectsZombies" ,
"RallyGroupSize" , "RallyTravelDistance" , "RallyGroupSeparation" , "RallyGroupRadius" ,
"HoursForCorpseRemoval" , "DecayingCorpseHealthImpact" , "BloodLevel" ,
"BloodSplatLifespanDays" ] ,
} ,
{
title : "Мир и время" ,
keys : [ "DayLength" , "StartYear" , "StartMonth" , "StartDay" , "StartTime" , "DayNightCycle" ,
"NightLength" , "NightDarkness" , "TimeSinceApo" , "WaterShut" , "ElecShut" ,
"WaterShutModifier" , "ElecShutModifier" , "Alarm" , "AlarmDecay" , "AlarmDecayModifier" ,
"LockedHouses" , "ErosionSpeed" , "ErosionDays" , "EndRegen" , "Helicopter" , "MetaEvent" ,
"SleepingEvent" , "AnnotatedMapChance" , "VehicleStoryChance" , "ZoneStoryChance" ,
"SurvivorHouseChance" , "MaximumLootedBuildingRooms" ] ,
} ,
{
title : "Погода" ,
keys : [ "ClimateCycle" , "FogCycle" , "Temperature" , "TemperatureModifier" , "Rain" ,
"RainModifier" , "MaxFogIntensity" , "MaxRainFxIntensity" , "EnableSnowOnGround" ] ,
} ,
{
title : "Лут и предметы" ,
keys : [ "LootRespawn" , "SeenHoursPreventLootRespawn" , "HoursForLootRespawn" ,
"MaxItemsForLootRespawn" , "ConstructionPreventsLootRespawn" , "RollsMultiplier" ,
"LootItemRemovalList" , "RemoveStoryLoot" , "RemoveZombieLoot" , "WorldItemRemovalList" ,
"HoursForWorldItemRemoval" , "ItemRemovalListBlacklistToggle" , "MaximumLooted" ,
"DaysUntilMaximumLooted" , "RuralLooted" , "MaximumDiminishedLoot" ,
"DaysUntilMaximumDiminishedLoot" , "SkillBookLoot" , "RecipeResourceLoot" ] ,
// *Loot, *LootNew и *LootFactor — десятки однотипных ключей.
prefixes : [ "Food" , "Canned" , "Literature" , "Medical" , "SurvivalGears" , "Weapon" ,
"RangedWeapon" , "Ammo" , "Mechanics" , "Other" , "Clothing" , "Container" , "Key" , "Media" ,
"Memento" , "Cookware" , "Material" , "Farming" , "Tool" ] ,
suffixes : [ "Loot" , "LootNew" , "LootFactor" ] ,
} ,
{
title : "Персонаж и навыки" ,
keys : [ "XpMultiplier" , "XpMultiplierAffectsPassive" , "StatsDecrease" , "Endurance" ,
"Nutrition" , "StarterKit" , "InjurySeverity" , "BoneFracture" , "CharacterFreePoints" ,
"ConstructionBonusPoints" , "NightVisionEnabled" , "InjuredHealingModifier" ,
"MuscleStrainFactor" , "DiscomfortFactor" , "WoundInfectionFactor" , "NegativeTraitsPenalty" ,
"EnablePoisoning" , "ClothingDegradation" , "AllClothesUnlocked" , "NoBlackClothes" ,
"EasyClimbing" , "AttackBlockMovements" , "MetaKnowledge" , "SeeNotLearntRecipe" ,
"LiteratureCooldown" , "MinutesPerPage" , "LevelForMediaXPCutoff" ,
"LevelForDismantleXPCutoff" ] ,
} ,
{
title : "Еда, вода и хозяйство" ,
keys : [ "FoodRotSpeed" , "FridgeFactor" , "MaggotSpawn" , "EnableTaintedWaterText" ,
"FishAbundance" , "NatureAbundance" , "PlantResilience" , "PlantAbundance" , "Farming" ,
"FarmingSpeedNew" , "FarmingAmountNew" , "CompostTime" , "KillInsideCrops" ,
"PlantGrowingSeasons" , "PlaceDirtAboveground" , "ClayLakeChance" , "ClayRiverChance" ,
"DaysForRottenFoodRemoval" , "AidPackage" ] ,
} ,
{
title : "Животные" ,
keys : [ "MaximumRatIndex" , "DaysUntilMaximumRatIndex" ] ,
prefixes : [ "Animal" ] ,
} ,
{
title : "Транспорт" ,
keys : [ "CarSpawnRate" , "ChanceHasGas" , "InitialGas" , "CarGasConsumption" , "LockedCar" ,
"CarGeneralCondition" , "CarDamageOnImpact" , "DamageToPlayerFromHitByACar" , "TrafficJam" ,
"CarAlarm" , "PlayerDamageFromCrash" , "SirenShutoffHours" , "RecentlySurvivorVehicles" ,
"EnableVehicles" , "VehicleEasyUse" , "MassInvasion" ] ,
prefixes : [ "FuelStation" ] ,
} ,
{
title : "Огнестрельное оружие" ,
keys : [ ] ,
prefixes : [ "Firearm" ] ,
} ,
{
title : "Электричество и огонь" ,
keys : [ "AllowExteriorGenerator" , "FireSpread" , "MaximumFireFuelHours" , "LightBulbLifespan" ] ,
prefixes : [ "Generator" ] ,
} ,
{
title : "Служебное" ,
keys : [ "VERSION" ] ,
} ,
] ;
let serverConfigValues = { } ;
async function loadServerConfig ( ) {
@@ -519,24 +1032,154 @@ async function loadServerConfig() {
return ;
}
serverConfigValues = data . values ;
const container = $ ( "#config-fields" ) ;
data . keys . forEach ( ( key ) => {
container . appendChild ( buildConfigField ( key , data . values [ key ] , CONFIG _HINTS [ key ] ) ) ;
} ) ;
const descriptions = data . descriptions || { } ;
const entries = data . keys . map ( ( key ) => ( {
key ,
value : data . values [ key ] ,
description : descriptions [ key ] ,
} ) ) ;
renderGroupedFields ( "#config-fields" , entries , CONFIG _GROUPS , CONFIG _HINTS ) ;
applyFieldFilter ( "#config-filter" , "#config-fields" ) ;
refreshFieldSaveState ( SAVE _TARGETS [ 0 ] ) ;
}
// groupEntries раскладывает параметры по разделам. Неизвестные ключи не
// теряются: они собираются в «Прочее» — конфиг PZ меняется от версии к версии.
function groupEntries ( entries , groups ) {
const byKey = new Map ( ) ;
entries . forEach ( ( entry ) => byKey . set ( entry . key , entry ) ) ;
const result = [ ] ;
const used = new Set ( ) ;
groups . forEach ( ( group ) => {
const items = [ ] ;
group . keys . forEach ( ( key ) => {
const entry = byKey . get ( key ) ;
if ( entry && ! used . has ( key ) ) {
used . add ( key ) ;
items . push ( entry ) ;
}
} ) ;
( group . prefixes || [ ] ) . forEach ( ( prefix ) => {
entries . forEach ( ( entry ) => {
if ( used . has ( entry . key ) ) return ;
// Для лута prefixes перечисляет вид припасов, suffixes — вид ключа
// (FoodLoot, FoodLootNew, RareLootFactor): совпасть должны оба.
const suffixes = group . suffixes ;
const matches = suffixes
? suffixes . some ( ( suffix ) => entry . key === prefix + suffix )
: entry . key . startsWith ( prefix ) ;
if ( matches ) {
used . add ( entry . key ) ;
items . push ( entry ) ;
}
} ) ;
} ) ;
// Суффиксы без своей пары в prefixes — например RareLootFactor.
( group . suffixes || [ ] ) . forEach ( ( suffix ) => {
entries . forEach ( ( entry ) => {
if ( ! used . has ( entry . key ) && entry . key . endsWith ( suffix ) ) {
used . add ( entry . key ) ;
items . push ( entry ) ;
}
} ) ;
} ) ;
if ( items . length ) result . push ( { title : group . title , items } ) ;
} ) ;
// Вложенные параметры песочницы сами собой образуют разделы: у них путь вида
// ZombieLore.Speed.
const nested = new Map ( ) ;
const rest = [ ] ;
entries . forEach ( ( entry ) => {
if ( used . has ( entry . key ) ) return ;
const dot = entry . key . indexOf ( "." ) ;
if ( dot > 0 ) {
const table = entry . key . slice ( 0 , dot ) ;
if ( ! nested . has ( table ) ) nested . set ( table , [ ] ) ;
nested . get ( table ) . push ( entry ) ;
return ;
}
rest . push ( entry ) ;
} ) ;
Array . from ( nested . entries ( ) )
. sort ( ( a , b ) => a [ 0 ] . localeCompare ( b [ 0 ] ) )
. forEach ( ( [ table , items ] ) => result . push ( { title : table , items } ) ) ;
if ( rest . length ) result . push ( { title : "Прочее" , items : rest } ) ;
return result ;
}
// Какие разделы человек раскрыл: вкладка перерисовывается при каждом переходе,
// и без этого раскрытый раздел схлопывался бы обратно.
const openedGroups = new Map ( ) ;
function rememberOpenGroups ( containerSelector ) {
const opened = new Set ( ) ;
$$ ( ` ${ containerSelector } .cfg-group ` ) . forEach ( ( group ) => {
if ( group . open ) opened . add ( group . dataset . group ) ;
} ) ;
openedGroups . set ( containerSelector , opened ) ;
}
// renderGroupedFields рисует разделы: свёрнутые по умолчанию, кроме первого —
// иначе сотня параметров снова превращается в простыню.
function renderGroupedFields ( containerSelector , entries , groups , hints ) {
const container = $ ( containerSelector ) ;
container . replaceChildren ( ) ;
const opened = openedGroups . get ( containerSelector ) ;
groupEntries ( entries , groups ) . forEach ( ( group , index ) => {
const section = document . createElement ( "details" ) ;
section . className = "cfg-group" ;
section . dataset . group = group . title ;
section . open = opened ? opened . has ( group . title ) : index === 0 ;
const summary = document . createElement ( "summary" ) ;
summary . innerHTML =
` ${ escapeHTML ( group . title ) } <span class="cfg-count"> ${ group . items . length } </span> ` ;
section . appendChild ( summary ) ;
const fields = document . createElement ( "div" ) ;
fields . className = "fields" ;
group . items . forEach ( ( entry ) => {
// Свой словарь важнее комментария из файла: игра пишет его на языке
// сервера, а он почти всегда английский.
let hint = hints ? hints [ entry . key ] : null ;
// Нумерованные проверки античита описывать поимённо нечем: игра не
// раскрывает, что стоит за каждым номером.
if ( ! hint && entry . key . startsWith ( "AntiCheatProtectionType" ) ) {
hint = [ "Античит: проверка " + entry . key . replace ( "AntiCheatProtectionType" , "№" ) ,
"Одна из встроенных проверок. Отключайте только при ложных срабатываниях." ] ;
}
const label = Array . isArray ( hint ) ? hint [ 0 ] : hint ;
const description = ( Array . isArray ( hint ) && hint [ 1 ] ) || entry . description ;
fields . appendChild ( buildConfigField ( entry . key , entry . value , label , description ) ) ;
} ) ;
section . appendChild ( fields ) ;
container . appendChild ( section ) ;
} ) ;
}
// buildConfigField выбирает тип поля по текущему значению: булевы значения
// удобнее переключать списком, числа — числовым полем.
function buildConfigField ( key , value , hint ) {
function buildConfigField ( key , value , hint , description ) {
const wrapper = document . createElement ( "div" ) ;
wrapper . className = "cfg-field" ;
wrapper . dataset . key = key . toLowerCase ( ) ;
wrapper . dataset . hint = ( hint || "" ) . toLowerCase ( ) ;
// Поиск идёт и по описанию: по-русски параметр искать привычнее, чем по
// английскому имени ключа.
wrapper . dataset . hint = ` ${ hint || "" } ${ description || "" } ` . toLowerCase ( ) ;
const label = document . createElement ( "label" ) ;
label . textContent = hint || key ;
if ( description ) {
// Полный текст — во всплывающей подсказке: описания бывают в несколько
// строк, и разворачивать их для каждого параметра значит утопить форму.
wrapper . title = description ;
}
wrapper . appendChild ( label ) ;
let input ;
@@ -569,6 +1212,13 @@ function buildConfigField(key, value, hint) {
keyLabel . className = "key" ;
keyLabel . textContent = key ;
wrapper . appendChild ( keyLabel ) ;
if ( description ) {
const hintLine = document . createElement ( "div" ) ;
hintLine . className = "hint" ;
hintLine . textContent = description ;
wrapper . appendChild ( hintLine ) ;
}
return wrapper ;
}
@@ -633,11 +1283,30 @@ $("#btn-config-save").addEventListener("click", async () => {
function applyFieldFilter ( inputSelector , containerSelector ) {
const filter = $ ( inputSelector ) . value . trim ( ) . toLowerCase ( ) ;
$$ ( ` ${ containerSelector } .cfg-field ` ) . forEach ( ( field ) => {
field . hidden = filter && ! field . dataset . key . includes ( filter ) && ! field . dataset . hint . includes ( filter ) ;
} ) ;
// Раздел без совпадений прячем целиком, а с совпадениями — раскрываем:
// искать параметр по свёрнутым разделам бессмысленно.
$$ ( ` ${ containerSelector } .cfg-group ` ) . forEach ( ( group ) => {
const visible = Array . from ( group . querySelectorAll ( ".cfg-field" ) ) . filter ( ( f ) => ! f . hidden ) ;
group . hidden = visible . length === 0 ;
if ( filter ) {
group . open = visible . length > 0 ;
}
const count = group . querySelector ( ".cfg-count" ) ;
if ( count ) {
count . textContent = filter ? ` ${ visible . length } ` : ` ${ group . querySelectorAll ( ".cfg-field" ) . length } ` ;
}
} ) ;
}
// toggle не всплывает, поэтому слушаем на фазе перехвата.
[ "#config-fields" , "#sandbox-fields" ] . forEach ( ( selector ) =>
$ ( selector ) . addEventListener ( "toggle" , ( ) => rememberOpenGroups ( selector ) , true ) ) ;
$ ( "#config-filter" ) . addEventListener ( "input" , ( ) => applyFieldFilter ( "#config-filter" , "#config-fields" ) ) ;
$ ( "#sandbox-filter" ) . addEventListener ( "input" , ( ) => applyFieldFilter ( "#sandbox-filter" , "#sandbox-fields" ) ) ;
@@ -656,10 +1325,12 @@ async function loadSandbox() {
refreshFieldSaveState ( SAVE _TARGETS [ 1 ] ) ;
return ;
}
const container = $ ( "#sandbox-fields" ) ;
data . entries . forEach ( ( entry ) => {
container . appendChild ( buildConfigField ( entry . path , entry . value , null ) ) ;
} ) ;
const entries = data . entries . map ( ( entry ) => ( {
key : entry . path ,
value : entry . value ,
description : entry . comment ,
} ) ) ;
renderGroupedFields ( "#sandbox-fields" , entries , SANDBOX _GROUPS , SANDBOX _HINTS ) ;
applyFieldFilter ( "#sandbox-filter" , "#sandbox-fields" ) ;
refreshFieldSaveState ( SAVE _TARGETS [ 1 ] ) ;
}