script_name('Хулиганы') script_author('Dormess') script_version('6.6.6') require 'lib.moonloader' local imgui = require 'mimgui' local encoding = require 'encoding' local json = require 'cjson' local sampev = require 'lib.samp.events' local ffi = require 'ffi' encoding.default = 'CP1251' local u8 = encoding.UTF8 local new = imgui.new local CONFIG = { command = 'nk', rulesPath = getWorkingDirectory() .. [[\Arizona Tools\rules.json]], logPath = getWorkingDirectory() .. [[\NK_Assistant.log]], warnlogCommand = '/warnlog_pl', warnlogCloseDelay = 80, warnlogTimeout = 2, maxMatches = 300, windowWidth = 980, windowHeight = 580 } local ui = { open = new.bool(false), centerNext = false } local state = { nick = '', query = '', matches = {}, selectedRule = 1, selectedVariant = 1, commandLine = '', error = '', confirmArmed = false, loadedRules = 0, loadedPunishments = 0, skippedRules = 0, lastAction = '', mode = 'reference', warnlog = { waiting = false, requestedNick = '', startedAt = 0, items = {}, match = nil, status = '' }, searchInput = new.char[256]() } local rules = {} local colors = { bg = imgui.ImVec4(0.025, 0.027, 0.031, 0.98), panel = imgui.ImVec4(0.050, 0.052, 0.058, 1.00), panel2 = imgui.ImVec4(0.120, 0.122, 0.130, 1.00), panel3 = imgui.ImVec4(0.180, 0.182, 0.192, 1.00), panelDeep = imgui.ImVec4(0.035, 0.037, 0.043, 1.00), border = imgui.ImVec4(0.255, 0.258, 0.270, 1.00), text = imgui.ImVec4(0.940, 0.940, 0.940, 1.00), muted = imgui.ImVec4(0.600, 0.610, 0.630, 1.00), accent = imgui.ImVec4(0.980, 0.980, 0.980, 1.00), accentSoft = imgui.ImVec4(0.350, 0.350, 0.360, 1.00), accentHover = imgui.ImVec4(0.450, 0.450, 0.465, 1.00), green = imgui.ImVec4(0.220, 0.560, 0.360, 1.00), red = imgui.ImVec4(0.760, 0.210, 0.180, 1.00), warning = imgui.ImVec4(0.920, 0.680, 0.300, 1.00) } local ruLowerMap = { ['А'] = 'а', ['Б'] = 'б', ['В'] = 'в', ['Г'] = 'г', ['Д'] = 'д', ['Е'] = 'е', ['Ё'] = 'е', ['Ж'] = 'ж', ['З'] = 'з', ['И'] = 'и', ['Й'] = 'й', ['К'] = 'к', ['Л'] = 'л', ['М'] = 'м', ['Н'] = 'н', ['О'] = 'о', ['П'] = 'п', ['Р'] = 'р', ['С'] = 'с', ['Т'] = 'т', ['У'] = 'у', ['Ф'] = 'ф', ['Х'] = 'х', ['Ц'] = 'ц', ['Ч'] = 'ч', ['Ш'] = 'ш', ['Щ'] = 'щ', ['Ъ'] = 'ъ', ['Ы'] = 'ы', ['Ь'] = 'ь', ['Э'] = 'э', ['Ю'] = 'ю', ['Я'] = 'я' } local profiles = { { keys = {'дм', 'dm'}, rules = {'4.2'}, reason = 'ДМ', terms = {'дм', 'deathmatch', 'нанесение урона без причины'} }, { keys = {'дб', 'db'}, rules = {'4.3'}, reason = 'ДБ', terms = {'дб', 'driveby', 'нанесение урона машиной'} }, { keys = {'ск', 'sk'}, rules = {'4.4'}, reason = 'СК', terms = {'ск', 'spawn kill', 'респауне'} }, { keys = {'тк', 'tk'}, rules = {'4.5'}, reason = 'ТК', terms = {'тк', 'team kill', 'союзникам'} }, { keys = {'пг', 'pg'}, rules = {'4.6'}, reason = 'ПГ', terms = {'пг', 'power gaming'} }, { keys = {'масс дм', 'mass dm', 'мдм'}, rules = {'4.7'}, reason = 'масс ДМ', terms = {'массовый', 'дм'} }, { keys = {'мг', 'mg'}, rules = {'5.2'}, reason = 'МГ', terms = {'мг', 'meta gaming'} }, { keys = {'капс', 'caps'}, rules = {'5.3'}, reason = 'капс', terms = {'капс'} }, { keys = {'флуд', 'flood'}, rules = {'5.4'}, reason = 'флуд', terms = {'флуд'} }, { keys = {'мат'}, rules = {'5.5'}, reason = 'мат', terms = {'мат', 'нецензур'} }, { keys = {'спам'}, rules = {'5.6'}, reason = 'спам', terms = {'спам', 'символами'} }, { keys = {'упом род', 'упом родных', 'упоминание род'}, rules = {'5.9'}, reason = 'упом.род', terms = {'упоминание родных'} }, { keys = {'оск род', 'оск родных', 'оскорбление родных'}, rules = {'5.10'}, reason = 'оск.род', terms = {'оскорбление родных'} }, { keys = {'см род', 'смерть родным', 'пожелание смерти родным'}, rules = {'5.11'}, reason = 'пож.смерти родным', terms = {'пожелание смерти родным'} }, { keys = {'оск адм', 'оск админ', 'оск администрации'}, rules = {'5.15'}, reason = 'оск.адм', terms = {'оскорбление администрации'} }, { keys = {'оск проект', 'оск серв', 'оск сервера'}, rules = {'5.13'}, reason = 'оск.проекта', terms = {'оскорбление сервера'} }, { keys = {'оффтоп репорт', 'offtop report'}, rules = {'5.12'}, reason = 'оффтоп в репорт', terms = {'оффтоп', 'report'} }, { keys = {'epp', 'епп', 'езда по полям'}, rules = {'4.1'}, reason = 'ЕПП', terms = {'езда по полям'} }, { keys = {'сбив'}, rules = {'4.12'}, reason = 'сбив', terms = {'сбив'} }, { keys = {'сбив выстрела'}, rules = {'4.13'}, reason = 'сбив выстрела', terms = {'сбив анимации выстрела'} }, { keys = {'афк рп', 'офф рп', 'отказ рп'}, rules = {'4.9'}, reason = 'уход от РП', terms = {'отказ от рп'} }, { keys = {'афк смерть', 'офф смерть', 'уход смерть'}, rules = {'4.10'}, reason = 'уход от смерти', terms = {'от смерти'} }, { keys = {'афк арест', 'офф арест', 'уход арест'}, rules = {'4.11'}, reason = 'уход от ареста', terms = {'от ареста'} }, { keys = {'дубинка в бою', 'тазер в бою', 'cuff в бою'}, rules = {'4.15'}, reason = 'дубинка/тазер в бою', terms = {'дубинку', 'тазер', 'бою'} }, { keys = {'чит', 'читы', 'cheat', 'cheats'}, rules = {'2.1.1', '2.1.2', '2.1.3', '2.1.4'}, reason = 'чит', bundle = 'cheats', terms = {'скрипты', 'моды', 'преимущество', 'обычные', 'значимые', 'тяжкие', 'вредительские', 'флай', 'спидхак', 'аим', 'вх', 'валшот', 'рапид', 'рванка', 'дамагер'} }, { keys = {'аим', 'aim'}, rules = {'2.1.2'}, reason = 'aim', terms = {'аим', 'aim'} }, { keys = {'вх', 'wh', 'wallhack'}, rules = {'2.1.2'}, reason = 'WH', terms = {'вх', 'wallhack'} }, { keys = {'флай', 'fly'}, rules = {'2.1.1'}, reason = 'fly', terms = {'флай'} }, { keys = {'спидхак', 'speedhack'}, rules = {'2.1.1'}, reason = 'speedhack', terms = {'спидхак'} }, { keys = {'рванка'}, rules = {'2.1.4'}, reason = 'рванка', terms = {'рванка'} }, { keys = {'бот', 'боты'}, rules = {'2.4'}, reason = 'бот', terms = {'бот'} } } local builtinRules = { { section = 'Lua test preset', rule = 'TEST', title = 'Тестовое наказание без JSON', punishment = 'Мут / Деморган на 10 минут с причиной тест.', variants = { { punishType = 'mute', command = 'mute', time = '10', label = 'Тест mute', source = 'Lua preset: mute 10 test', complex = false, reasonOverride = 'тест' }, { punishType = 'jail', command = 'jail', time = '10', label = 'Тест jail', source = 'Lua preset: jail 10 test', complex = false, reasonOverride = 'тест' } }, search = 'тест test lua preset ban mute jail бан мут деморган', words = {'тест', 'test', 'lua', 'preset', 'ban', 'mute', 'jail', 'бан', 'мут', 'деморган'}, wordSet = {['тест'] = true, ['test'] = true, ['lua'] = true, ['preset'] = true, ['ban'] = true, ['mute'] = true, ['jail'] = true, ['бан'] = true, ['мут'] = true, ['деморган'] = true}, sort = 0, virtual = true, testPreset = true } } local typeInfo = { ban = { title = 'Бан', command = 'ban', unit = 'дн.', color = colors.accentSoft }, jail = { title = 'Деморган', command = 'jail', unit = 'мин.', color = colors.accentSoft }, mute = { title = 'Мут', command = 'mute', unit = 'мин.', color = colors.accentSoft }, warn = { title = 'Варн', command = 'warn', unit = '', color = colors.accentSoft }, sbanip = { title = 'IP-бан', command = 'sbanip', unit = '', color = colors.accentSoft } } local function trim(text) local result = (text or ''):gsub('^%s+', ''):gsub('%s+$', '') return result end local function bytes(...) return string.char(...) end local WL_RU = { razglushil = bytes(240, 224, 231, 227, 235, 243, 248, 232, 235), Razglushil = bytes(208, 224, 231, 227, 235, 243, 248, 232, 235), razbanil = bytes(240, 224, 231, 225, 224, 237, 232, 235), Razbanil = bytes(208, 224, 231, 225, 224, 237, 232, 235), snyalZaglushku = bytes(241, 237, 255, 235, 32, 231, 224, 227, 235, 243, 248, 234, 243), SnyalZaglushku = bytes(209, 237, 255, 235, 32, 231, 224, 227, 235, 243, 248, 234, 243), vypustil = bytes(226, 251, 239, 243, 241, 242, 232, 235), Vypustil = bytes(194, 251, 239, 243, 241, 242, 232, 235), vernu = bytes(226, 229, 240, 237, 243), Vernu = bytes(194, 229, 240, 237, 243), naVremya = bytes(237, 224, 32, 226, 240, 229, 236, 255), NaVremya = bytes(205, 224, 32, 226, 240, 229, 236, 255), oshibka = bytes(238, 248, 232, 225, 234, 224), Oshibka = bytes(206, 248, 232, 225, 234, 224), prichina = bytes(239, 240, 232, 247, 232, 237, 224, 58), Prichina = bytes(207, 240, 232, 247, 232, 237, 224, 58) } local function chat(text, color) sampAddChatMessage(u8:decode('[NK] ' .. tostring(text)), color or 0x7FD6FFFF) end local function logLine(text) local file = io.open(CONFIG.logPath, 'a') if not file then return end file:write(string.format('[%s] %s\n', os.date('%Y-%m-%d %H:%M:%S'), u8:decode(tostring(text)))) file:close() end local function safeCp1251(text) return u8:decode(text or '') end local function ruLower(text) text = (text or ''):lower() for upper, lower in pairs(ruLowerMap) do text = text:gsub(upper, lower) end return text end local function stripColors(text) return (text or ''):gsub('{%x%x%x%x%x%x}', ''):gsub('{STANDART}', '') end local function normalize(text) text = ruLower(stripColors(text or '')) text = text:gsub('ё', 'е') text = text:gsub('[%p%c]', ' ') text = text:gsub('%s+', ' ') return trim(text) end local function splitWords(text) local words = {} for word in normalize(text):gmatch('%S+') do if #word > 1 then table.insert(words, word) end end return words end local function wordSet(text) local set = {} local list = splitWords(text) for _, word in ipairs(list) do set[word] = true end return set, list end local function readFile(path) local file = io.open(path, 'rb') if not file then return nil end local data = file:read('*a') file:close() return data end local function setSearchInput(value) value = tostring(value or '') local size = ffi.sizeof(state.searchInput) ffi.fill(state.searchInput, size) ffi.copy(state.searchInput, value, math.min(#value, size - 1)) end local function decodeRulesJson(raw) local ok, data = pcall(json.decode, raw) if ok and type(data) == 'table' and raw:find('Наказание', 1, true) then return data end local converted = u8(raw) ok, data = pcall(json.decode, converted) if ok and type(data) == 'table' then return data end return nil end local function getRuleNumber(line) return (line or ''):match('(%d+%.%d+%.?%d*)') end local function ruleSortValue(rule) local a, b, c = tostring(rule or ''):match('^(%d+)%.(%d+)%.?(%d*)$') a, b, c = tonumber(a) or 99, tonumber(b) or 99, tonumber(c) or 0 return a * 10000 + b * 100 + c end local function cleanReasonTitle(line, ruleNumber) local text = stripColors(line or '') text = text:gsub('^%s*%-?%s*', '') if ruleNumber and ruleNumber ~= '?' then text = text:gsub('^' .. ruleNumber:gsub('%.', '%%.') .. '%.?%s*', '') end text = text:gsub('%s*%-%s*Наказание:.*$', '') text = text:gsub('%s+', ' ') return trim(text) end local function cleanRuleNote(line) local text = stripColors(line or '') text = text:gsub('^%s*%-+%s*', '') text = text:gsub('%s+', ' ') return trim(text) end local function rebuildRuleIndex(rule) local parts = { rule.section or '', rule.rule or '', rule.title or '', rule.punishment or '', rule.fullLine or '' } for _, note in ipairs(rule.notes or {}) do parts[#parts + 1] = note end rule.search = normalize(table.concat(parts, ' ')) rule.wordSet, rule.words = wordSet(rule.search) end local function appendRuleNote(rule, line) if not rule then return end local note = cleanRuleNote(line) if #note == 0 or note:sub(1, 1) == '#' then return end rule.notes = rule.notes or {} rule.notes[#rule.notes + 1] = note rebuildRuleIndex(rule) end local function getPunishmentText(line) return trim(stripColors(line or ''):match('Наказание:%s*(.+)') or '') end local function splitPunishment(punishment) local text = trim((punishment or ''):gsub('%.%s*$', '')) text = text:gsub('%s+%-+%s+', '\31') local parts = {} for part in text:gmatch('[^\31]+') do part = trim(part) if #part > 0 then table.insert(parts, part) end end return parts end local function detectType(fragment) local low = normalize(fragment) if low:find('мут репорта', 1, true) then return nil end if low:find('бан фа', 1, true) then return nil end if low:find('деморган', 1, true) then return 'jail' end if low:find('мут', 1, true) then return 'mute' end if low:find('варн', 1, true) then return 'warn' end if low:find('бан', 1, true) then return 'ban' end return nil end local function hasUnsupportedPunishment(fragment) local low = normalize(fragment) return low:find('кик', 1, true) ~= nil or low:find('устное', 1, true) ~= nil or low:find('предупреждение', 1, true) ~= nil or low:find('спавн', 1, true) ~= nil or low:find('очист', 1, true) ~= nil or low:find('изъят', 1, true) ~= nil end local function extractTime(fragment, punishType) local low = normalize(fragment) local minutes = low:match('(%d+)%s*мин') local days = low:match('(%d+)%s*д') if punishType == 'ban' then return days or minutes or low:match('(%d+)') end if punishType == 'jail' or punishType == 'mute' then return minutes or days or low:match('(%d+)') end return low:match('(%d+)') end local function variantLabel(index, part) local low = normalize(part) if low:find('повтор', 1, true) then return 'Повторное' end if low:find('от ', 1, true) then return 'Минимум' end if index == 1 then return 'Первое' end return 'Вариант ' .. tostring(index) end local function buildSbanipVariant(label, reason, ruleNumber, source) return { punishType = 'sbanip', command = 'sbanip', time = nil, label = label, source = source, complex = false, reasonOverride = reason, ruleOverride = ruleNumber } end local function parseVariants(punishment) local variants = {} local lastType = nil for index, part in ipairs(splitPunishment(punishment)) do local explicitType = detectType(part) local unsupported = explicitType == nil and hasUnsupportedPunishment(part) local punishType = explicitType or (not unsupported and lastType or nil) if explicitType then lastType = explicitType elseif unsupported then lastType = nil end if punishType == 'ban' and extractTime(part, punishType) == '998' then variants[#variants + 1] = buildSbanipVariant( normalize(part):find('бот', 1, true) and 'Бот' or 'Вредитель', normalize(part):find('бот', 1, true) and 'Бот' or 'Вредитель', nil, part ) elseif punishType == 'warn' then variants[#variants + 1] = { punishType = 'warn', command = 'warn', time = nil, label = variantLabel(index, part), source = part, complex = false } elseif punishType and typeInfo[punishType] then local time = extractTime(part, punishType) if time and tonumber(time) and tonumber(time) > 0 and time ~= '998' then table.insert(variants, { punishType = punishType, command = typeInfo[punishType].command, time = tostring(tonumber(time)), label = variantLabel(index, part), source = part, complex = part:find('%+') ~= nil or normalize(part):find('запрет', 1, true) ~= nil }) end end end return variants end local function buildRule(sectionName, line, fallbackRule) local ruleNumber = getRuleNumber(line) or fallbackRule or '?' local punishment = getPunishmentText(line) local variants = parseVariants(punishment) local title = cleanReasonTitle(line, ruleNumber) local fullLine = stripColors(line or '') for _, variant in ipairs(variants) do if variant.punishType == 'sbanip' and normalize(fullLine):find('бот', 1, true) then variant.label = 'Бот' variant.reasonOverride = 'Бот' end end local rule = { section = sectionName, rule = ruleNumber, title = title, punishment = punishment, hasPunishment = #punishment > 0, variants = variants, notes = {}, fullLine = fullLine, sort = ruleSortValue(ruleNumber) } rebuildRuleIndex(rule) return rule end local function loadRules() rules = {} state.skippedRules = 0 state.loadedPunishments = 0 for _, rule in ipairs(builtinRules) do rules[#rules + 1] = rule if rule.variants and #rule.variants > 0 then state.loadedPunishments = state.loadedPunishments + #rule.variants end end local raw = readFile(CONFIG.rulesPath) if not raw then return false, 'Не найден файл правил: ' .. CONFIG.rulesPath end local data = decodeRulesJson(raw) if type(data) ~= 'table' then return false, 'Не получилось прочитать JSON с правилами' end local function addRule(rule) if not rule then return end table.insert(rules, rule) if rule.variants and #rule.variants > 0 then state.loadedPunishments = state.loadedPunishments + #rule.variants elseif rule.hasPunishment then state.skippedRules = state.skippedRules + 1 end end for _, section in ipairs(data) do if type(section) == 'table' and type(section.content) == 'table' then local sectionName = section.name or '' local currentRule = nil local currentRuleObject = nil for _, line in ipairs(section.content) do if type(line) == 'string' then local number = getRuleNumber(line) if number then currentRule = number currentRuleObject = buildRule(sectionName, line, currentRule) addRule(currentRuleObject) else local note = cleanRuleNote(line) if #note == 0 or note:sub(1, 1) == '#' then currentRuleObject = nil else appendRuleNote(currentRuleObject, line) end end if not number and line:find('Наказание:', 1, true) then addRule(buildRule(sectionName, line, currentRule)) end end end end end state.loadedRules = #rules return #rules > 0, #rules > 0 and nil or 'В файле правил не найдено ни одного пункта' end local function queryHasPhrase(queryNorm, phrase) phrase = normalize(phrase) if #phrase == 0 then return false end return (' ' .. queryNorm .. ' '):find(' ' .. phrase .. ' ', 1, true) ~= nil or queryNorm == phrase end local function activeProfiles(query) local found = {} local queryNorm = normalize(query) for _, profile in ipairs(profiles) do for _, key in ipairs(profile.keys) do if queryHasPhrase(queryNorm, key) then table.insert(found, profile) break end end end return found end local function expandQueryTerms(query, matchedProfiles) local terms = {} for _, word in ipairs(splitWords(query)) do table.insert(terms, word) end for _, profile in ipairs(matchedProfiles) do for _, term in ipairs(profile.terms or {}) do table.insert(terms, normalize(term)) end end return terms end local function containsTerm(rule, term) term = normalize(term) if #term == 0 then return false, 0 end if term:find(' ', 1, true) then return rule.search:find(term, 1, true) ~= nil, 12 end if rule.wordSet[term] then return true, #term <= 3 and 14 or 10 end if #term >= 4 then for _, word in ipairs(rule.words) do if word:find(term, 1, true) == 1 then return true, 7 end end if rule.search:find(term, 1, true) then return true, 4 end end return false, 0 end local function scoreRule(rule, query, matchedProfiles) local queryNorm = normalize(query) local score = 0 local hits = 0 local askedRule = tostring(query or ''):match('(%d+%.%d+%.?%d*)') if askedRule then if rule.rule == askedRule then score = score + 220 elseif rule.rule:find('^' .. askedRule:gsub('%.', '%%.')) then score = score + 80 end end for _, profile in ipairs(matchedProfiles) do for _, ruleNumber in ipairs(profile.rules or {}) do if rule.rule == ruleNumber then score = score + 160 end end end for _, term in ipairs(expandQueryTerms(query, matchedProfiles)) do local ok, add = containsTerm(rule, term) if ok then score = score + add hits = hits + 1 end end if hits > 0 then score = score + math.min(hits, 6) * 3 end if #matchedProfiles > 0 and score < 100 then score = math.floor(score * 0.65) end return score end local function findRules(query) local matches = {} local matchedProfiles = activeProfiles(query) local queryNorm = normalize(query) local function hasBundle(name) for _, profile in ipairs(matchedProfiles) do if profile.bundle == name then return true end end return false end local function findRuleByNumber(number) for _, rule in ipairs(rules) do if rule.rule == number then return rule end end return nil end if #queryNorm == 0 then for _, rule in ipairs(rules) do if not rule.testPreset then matches[#matches + 1] = { rule = rule, score = 0, confidence = 0 } end end table.sort(matches, function(a, b) return a.rule.sort < b.rule.sort end) local out = {} for i = 1, math.min(#matches, CONFIG.maxMatches) do table.insert(out, matches[i]) end return out end if hasBundle('cheats') then local bundle = { section = 'Основные положения проекта', rule = '2.1.1-2.1.4', title = 'Использование читов / скриптов / модов, дающих преимущество', punishment = 'Бан 15 / 30 / 998 дней в зависимости от категории чита.', variants = {}, search = 'чит читы скрипты моды преимущество бан 15 30 998', words = {'чит', 'читы', 'скрипты', 'моды', 'преимущество', 'бан'}, wordSet = {['чит'] = true, ['читы'] = true, ['скрипты'] = true, ['моды'] = true, ['преимущество'] = true, ['бан'] = true}, sort = ruleSortValue('2.1.1'), virtual = true } local labels = { ['2.1.1'] = 'Обычные читы', ['2.1.2'] = 'Значимые читы', ['2.1.3'] = 'Тяжкие читы', ['2.1.4'] = 'Вредительские читы' } for _, ruleNumber in ipairs({'2.1.1', '2.1.2', '2.1.3', '2.1.4'}) do local rule = findRuleByNumber(ruleNumber) if rule and rule.variants[1] then local variant = rule.variants[1] bundle.variants[#bundle.variants + 1] = { punishType = variant.punishType, command = variant.command, time = variant.time, label = labels[ruleNumber] or rule.title, source = rule.title .. ' - ' .. rule.punishment, complex = variant.complex, reasonOverride = variant.reasonOverride, ruleOverride = ruleNumber } end end if #bundle.variants > 0 then matches[#matches + 1] = { rule = bundle, score = 500, confidence = 99 } end end for _, rule in ipairs(rules) do local score = scoreRule(rule, query, matchedProfiles) if score > 0 then table.insert(matches, { rule = rule, score = score, confidence = math.min(99, math.max(1, math.floor(score / 2))) }) end end table.sort(matches, function(a, b) if a.score == b.score then return a.rule.sort < b.rule.sort end return a.score > b.score end) local out = {} for i = 1, math.min(#matches, CONFIG.maxMatches) do table.insert(out, matches[i]) end return out end local function selectedMatch() return state.matches[state.selectedRule] end local function selectedRule() local match = selectedMatch() return match and match.rule or nil end local function selectedVariant() local rule = selectedRule() if not rule then return nil end return rule.variants[state.selectedVariant] end local function canonicalReason(query, rule) local matched = activeProfiles(query) for _, profile in ipairs(matched) do for _, ruleNumber in ipairs(profile.rules or {}) do if rule and rule.rule == ruleNumber and profile.reason then return profile.reason end end end local normalized = normalize(query) for _, profile in ipairs(matched) do if profile.reason then return profile.reason end end local compact = trim((query or ''):gsub('%s+', ' ')) if #compact == 0 then compact = rule and rule.title or 'нарушение' end if normalized == 'dm' or normalized == 'дм' then return 'ДМ' end if normalized == 'db' or normalized == 'дб' then return 'ДБ' end if normalized == 'sk' or normalized == 'ск' then return 'СК' end if normalized == 'tk' or normalized == 'тк' then return 'ТК' end if normalized == 'pg' or normalized == 'пг' then return 'ПГ' end if normalized == 'mg' or normalized == 'мг' then return 'МГ' end return compact end local function makeReason(rule, variant) if variant and variant.reasonOverride then return variant.reasonOverride end local reason = canonicalReason(state.query, rule) local ruleNumber = variant and variant.ruleOverride or (rule and rule.rule) if ruleNumber and ruleNumber ~= '?' then reason = string.format('%s (ПП %s)', reason, ruleNumber) end return reason end local function warnlogContainsIgnoredAction(line) return line:find(WL_RU.razglushil, 1, true) or line:find(WL_RU.Razglushil, 1, true) or line:find(WL_RU.razbanil, 1, true) or line:find(WL_RU.Razbanil, 1, true) or line:find(WL_RU.snyalZaglushku, 1, true) or line:find(WL_RU.SnyalZaglushku, 1, true) or line:find(WL_RU.vypustil, 1, true) or line:find(WL_RU.Vypustil, 1, true) end local function warnlogContainsIgnoredReason(reason) return reason:find(WL_RU.vernu, 1, true) or reason:find(WL_RU.Vernu, 1, true) or reason:find(WL_RU.naVremya, 1, true) or reason:find(WL_RU.NaVremya, 1, true) or reason:find(WL_RU.vypustil, 1, true) or reason:find(WL_RU.Vypustil, 1, true) or reason:find(WL_RU.oshibka, 1, true) or reason:find(WL_RU.Oshibka, 1, true) end local function warnlogParseDateTime(line) local y, mo, d, h, mi, s = line:match('%[(%d%d%d%d)%-(%d%d)%-(%d%d)%s+(%d%d):(%d%d):(%d%d)%]') if not y then return nil end return os.time({ year = tonumber(y), month = tonumber(mo), day = tonumber(d), hour = tonumber(h), min = tonumber(mi), sec = tonumber(s) }) end local function warnlogParseReason(line) local _, endPos = line:find(WL_RU.prichina, 1, true) if not endPos then _, endPos = line:find(WL_RU.Prichina, 1, true) end if not endPos then return nil end local reason = line:sub(endPos + 1) reason = reason:gsub('%s*//.*$', '') reason = reason:gsub('^%s+', ''):gsub('%s+$', '') reason = reason:gsub('%.$', '') return reason end local function warnlogParseItems(dialogText) local items = {} for rawLine in (dialogText or ''):gmatch('[^\r\n]+') do local line = rawLine:gsub('{%x%x%x%x%x%x}', '') if not warnlogContainsIgnoredAction(line) then local timestamp = warnlogParseDateTime(line) local reasonCp = warnlogParseReason(line) if timestamp and reasonCp and #reasonCp > 0 and not warnlogContainsIgnoredReason(reasonCp) then items[#items + 1] = { timestamp = timestamp, reason = u8(reasonCp) } end end end table.sort(items, function(a, b) return a.timestamp > b.timestamp end) return items end local function cleanReasonForCompare(reason) local value = normalize(reason or '') value = value:gsub('пп%s*%d+%s*%d*%s*%d*', ' ') value = value:gsub('%s+', ' ') return trim(value) end local function samePunishReason(a, b) a, b = cleanReasonForCompare(a), cleanReasonForCompare(b) if #a == 0 or #b == 0 then return false end if a == b then return true end return (' ' .. a .. ' '):find(' ' .. b .. ' ', 1, true) ~= nil or (' ' .. b .. ' '):find(' ' .. a .. ' ', 1, true) ~= nil end local function formatDaysAgo(timestamp) local diff = math.max(0, os.time() - timestamp) local days = math.floor(diff / 86400) local hours = math.floor((diff % 86400) / 3600) if days > 0 then return string.format('%d дн. назад', days) end return string.format('%d ч. назад', hours) end local function updateWarnlogMatch() local rule = selectedRule() local variant = selectedVariant() state.warnlog.match = nil if state.mode ~= 'punish' or not rule or not variant then return end local targetReason = makeReason(rule, variant) for _, item in ipairs(state.warnlog.items or {}) do if samePunishReason(item.reason, targetReason) then state.warnlog.match = item return end end end local function requestWarnlog() state.warnlog.waiting = false state.warnlog.items = {} state.warnlog.match = nil state.warnlog.status = '' if state.mode ~= 'punish' or #state.nick == 0 then return end state.warnlog.waiting = true state.warnlog.requestedNick = state.nick state.warnlog.startedAt = os.time() state.warnlog.status = 'Загружаю warnlog...' sampSendChat(CONFIG.warnlogCommand .. ' ' .. state.nick) end function sampev.onShowDialog(dialogId, style, title, button1, button2, text) if state.warnlog.waiting then state.warnlog.waiting = false state.warnlog.items = warnlogParseItems(text or '') updateWarnlogMatch() if state.warnlog.match then state.warnlog.status = 'Найдено похожее наказание' else state.warnlog.status = 'Такой причины в warnlog не найдено' end lua_thread.create(function() wait(CONFIG.warnlogCloseDelay) pcall(sampCloseCurrentDialogWithButton, 0) end) end end local function rebuildCommand() local rule = selectedRule() local variant = selectedVariant() state.commandLine = '' if state.mode ~= 'punish' or not rule or not variant or #state.nick == 0 then return end if variant.punishType == 'sbanip' or variant.punishType == 'warn' then state.commandLine = string.format('/%s %s %s', variant.command, state.nick, makeReason(rule, variant)) else state.commandLine = string.format( '/%s %s %s %s', variant.command, state.nick, variant.time, makeReason(rule, variant) ) end end local function setSelection(index) if not state.matches[index] then return end state.selectedRule = index state.selectedVariant = 1 state.confirmArmed = false rebuildCommand() updateWarnlogMatch() end local function setVariant(index) local rule = selectedRule() if not rule or not rule.variants[index] then return end state.selectedVariant = index state.confirmArmed = false rebuildCommand() updateWarnlogMatch() end local function refreshMatches(query, alreadyUtf8, updateInput, quiet) query = trim(query or '') state.query = alreadyUtf8 and query or u8(query) state.matches = findRules(state.query) state.selectedRule = 1 state.selectedVariant = 1 state.confirmArmed = false state.error = '' state.commandLine = '' if updateInput then setSearchInput(state.query) end if #state.matches == 0 then if #state.query == 0 then state.error = 'Правила не загружены или список пустой' else state.error = 'Ничего не найдено. Попробуй точнее: ДМ, оск род, МГ, 4.2' if not quiet then chat('По запросу "' .. state.query .. '" ничего не найдено', 0xFF7777FF) end end return end rebuildCommand() updateWarnlogMatch() end local function openLookup(query, mode, nick) query = trim(query or '') if #query == 0 and mode == 'punish' then chat('Используй: /nk Nick_Name причина или /nk причина для справочника', 0xFFCC66FF) return end state.mode = mode or 'reference' state.nick = nick or '' state.lastAction = '' refreshMatches(query, false, true, false) ui.centerNext = true ui.open[0] = true requestWarnlog() end local function isPunishTarget(value) if not value or #value == 0 then return false end local id = value:match('^(%d%d?%d?)$') if id then id = tonumber(id) return id ~= nil and id >= 0 and id <= 999 end return value:find('_', 1, true) ~= nil end local function openPunish(params) params = trim(params or '') if #params == 0 then openLookup('', 'reference', '') return end local target, query = params:match('^(%S+)%s+(.+)$') if target and query and isPunishTarget(target) then openLookup(query, 'punish', target) else openLookup(params, 'reference', '') end end local function shortText(text, limit) text = tostring(text or '') if #text <= limit then return text end return text:sub(1, limit - 3) .. '...' end local function formatRuleChatLine(rule, confidence) if not rule then return '' end local line = 'ПП ' .. tostring(rule.rule) .. ': ' .. shortText(rule.title, 90) if rule.punishment and #rule.punishment > 0 then line = line .. ' | Наказание: ' .. shortText(rule.punishment, 95) else line = line .. ' | наказание не указано' end if confidence and confidence > 0 then line = line .. ' | совпадение ' .. tostring(confidence) .. '%' end return line end local function sendQuickReference(params) params = trim(params or '') if #params == 0 then chat('Используй: /nks правило. Например: /nks ДМ или /nks оск род', 0xFFCC66FF) return end local matches = findRules(u8(params)) if #matches == 0 then chat('По запросу "' .. u8(params) .. '" ничего не найдено', 0xFF7777FF) return end chat(formatRuleChatLine(matches[1].rule, matches[1].confidence)) end local function issueCommand() if state.mode ~= 'punish' then state.lastAction = 'Выдача отключена: режим справочника' return end if #state.commandLine == 0 then return end sampSendChat(safeCp1251(state.commandLine)) logLine('ISSUE ' .. state.commandLine) state.lastAction = 'Команда отправлена: ' .. state.commandLine state.confirmArmed = false ui.open[0] = false end local function putCommandToChat() if state.mode ~= 'punish' then state.lastAction = 'Вставка отключена: режим справочника' return end if #state.commandLine == 0 then return end pcall(sampSetChatInputEnabled, true) pcall(sampSetChatInputText, safeCp1251(state.commandLine)) logLine('CHAT_INPUT ' .. state.commandLine) state.lastAction = 'Строка вставлена в чат' end local function copyCommand() if state.mode ~= 'punish' then state.lastAction = 'Копирование команды отключено: режим справочника' return end if #state.commandLine == 0 then return end pcall(imgui.SetClipboardText, state.commandLine) state.lastAction = 'Строка скопирована' end local function pushTheme() imgui.PushStyleColor(imgui.Col.WindowBg, colors.bg) imgui.PushStyleColor(imgui.Col.ChildBg, colors.panel) imgui.PushStyleColor(imgui.Col.Border, colors.border) imgui.PushStyleColor(imgui.Col.Text, colors.text) imgui.PushStyleColor(imgui.Col.TextDisabled, colors.muted) imgui.PushStyleColor(imgui.Col.FrameBg, colors.panel2) imgui.PushStyleColor(imgui.Col.Button, colors.panel3) imgui.PushStyleColor(imgui.Col.ButtonHovered, colors.accentSoft) imgui.PushStyleColor(imgui.Col.ButtonActive, colors.accentHover) imgui.PushStyleColor(imgui.Col.Header, colors.panel3) imgui.PushStyleColor(imgui.Col.HeaderHovered, colors.accentSoft) imgui.PushStyleColor(imgui.Col.HeaderActive, colors.accentHover) imgui.PushStyleColor(imgui.Col.TitleBg, colors.bg) imgui.PushStyleColor(imgui.Col.TitleBgActive, colors.bg) imgui.PushStyleVarFloat(imgui.StyleVar.WindowRounding, 8) imgui.PushStyleVarFloat(imgui.StyleVar.ChildRounding, 6) imgui.PushStyleVarFloat(imgui.StyleVar.FrameRounding, 5) imgui.PushStyleVarFloat(imgui.StyleVar.ScrollbarRounding, 4) imgui.PushStyleVarFloat(imgui.StyleVar.WindowBorderSize, 1) imgui.PushStyleVarFloat(imgui.StyleVar.ChildBorderSize, 1) imgui.PushStyleVarVec2(imgui.StyleVar.WindowPadding, imgui.ImVec2(10, 10)) imgui.PushStyleVarVec2(imgui.StyleVar.FramePadding, imgui.ImVec2(10, 7)) imgui.PushStyleVarVec2(imgui.StyleVar.ItemSpacing, imgui.ImVec2(8, 7)) end local function popTheme() imgui.PopStyleVar(9) imgui.PopStyleColor(14) end local function textRaw(text) imgui.TextUnformatted(tostring(text or '')) end local function textWrappedRaw(text) imgui.PushTextWrapPos(0) imgui.TextUnformatted(tostring(text or '')) imgui.PopTextWrapPos() end local function textColor(color, text) imgui.PushStyleColor(imgui.Col.Text, color) textRaw(text) imgui.PopStyleColor() end local function tooltip(text) imgui.SetTooltip(tostring(text or ''):gsub('%%', '%%%%')) end local function textMuted(text) textColor(colors.muted, text) end local function textAccent(text) textColor(colors.accent, text) end local function chip(label, color) local chipColor = color or colors.accentSoft imgui.PushStyleColor(imgui.Col.Button, chipColor) imgui.PushStyleColor(imgui.Col.ButtonHovered, chipColor) imgui.PushStyleColor(imgui.Col.ButtonActive, chipColor) imgui.Button(label, imgui.ImVec2(0, 24)) imgui.PopStyleColor(3) end local function metric(label, value) textMuted(label) imgui.SameLine(0, 6) textRaw(value) end local function panelButton(label, size, active, color) if active then imgui.PushStyleColor(imgui.Col.Button, color or colors.accentSoft) imgui.PushStyleColor(imgui.Col.ButtonHovered, colors.accentHover) imgui.PushStyleColor(imgui.Col.ButtonActive, colors.accentHover) end local clicked = imgui.Button(label, size) if active then imgui.PopStyleColor(3) end return clicked end local function infoBox(id, title, value) imgui.PushStyleColor(imgui.Col.ChildBg, colors.panelDeep) imgui.BeginChild(id, imgui.ImVec2(0, 50), true, imgui.WindowFlags.NoScrollbar) textMuted(title) textWrappedRaw(value) imgui.EndChild() imgui.PopStyleColor() end local function drawSidebar() imgui.BeginChild('sidebar', imgui.ImVec2(170, 0), true, imgui.WindowFlags.NoScrollbar) if state.mode == 'punish' then infoBox('side_mode', 'Режим', 'Выдача наказания') infoBox('side_target', 'Игрок', state.nick) else infoBox('side_mode', 'Режим', 'Справочник') end infoBox('side_query', 'Запрос', #state.query > 0 and state.query or 'все правила') imgui.Spacing() infoBox('side_rules', 'Правил', tostring(state.loadedRules)) infoBox('side_found', 'Найдено', tostring(#state.matches)) imgui.Spacing() if panelButton('Обновить базу', imgui.ImVec2(0, 32), true, colors.accentSoft) then local ok, err = loadRules() if ok then state.matches = findRules(state.query) setSelection(1) state.lastAction = 'База правил обновлена' else state.error = err or 'Ошибка обновления базы' end end imgui.EndChild() end local function drawHeader() imgui.BeginChild('header', imgui.ImVec2(0, 102), true, imgui.WindowFlags.NoScrollbar) textAccent('Помощник для наказаний') imgui.SameLine() textMuted('by dormess') imgui.Spacing() textMuted('Поиск правила') imgui.SetNextItemWidth(-1) if imgui.InputText('##rule_search', state.searchInput, ffi.sizeof(state.searchInput)) then refreshMatches(ffi.string(state.searchInput), true, false, true) end imgui.EndChild() end local function drawMatches() imgui.BeginChild('matches_panel', imgui.ImVec2(0, 0), true) textAccent('Результаты поиска') textMuted('точные совпадения и похожие пункты') imgui.Separator() if #state.matches == 0 then imgui.Spacing() textWrappedRaw(state.error) imgui.Spacing() textMuted('Примеры: ДМ, оск род, МГ, 4.2, аим, флуд') else for i, item in ipairs(state.matches) do local selected = i == state.selectedRule local confidence = item.confidence > 0 and string.format(' [%d%%] ', item.confidence) or ' ' local marker = (#item.rule.variants > 0) and '>' or 'i' local label = string.format('%s %s%s%s##match%d', marker, item.rule.rule, confidence, item.rule.title, i) if imgui.Selectable(label, selected, 0, imgui.ImVec2(0, 42)) then setSelection(i) end if imgui.IsItemHovered() then local punish = (item.rule.punishment and #item.rule.punishment > 0) and item.rule.punishment or 'Наказание не указано' tooltip(item.rule.section .. '\n' .. punish) end end end imgui.EndChild() end local function drawVariantButton(index, variant, active) local info = typeInfo[variant.punishType] local label if variant.punishType == 'sbanip' or variant.punishType == 'warn' then label = string.format('%s: /%s %s##variant%d', variant.label, variant.command, makeReason(selectedRule(), variant), index) else label = string.format('%s: %s %s %s##variant%d', variant.label, info.title, variant.time, info.unit, index) end if active then imgui.PushStyleColor(imgui.Col.Button, colors.accentSoft) imgui.PushStyleColor(imgui.Col.ButtonHovered, colors.accentHover) imgui.PushStyleColor(imgui.Col.ButtonActive, colors.accentHover) end if imgui.Button(label, imgui.ImVec2(0, 34)) then setVariant(index) end if active then imgui.PopStyleColor(3) end if imgui.IsItemHovered() then tooltip(variant.source) end end local function drawWarnlogHint(rule, variant) if state.mode ~= 'punish' then return end imgui.BeginChild('warnlog_hint', imgui.ImVec2(0, 58), true) textAccent('Warnlog') imgui.SameLine() if state.warnlog.waiting then textMuted('загружаю последние наказания...') elseif state.warnlog.match then local item = state.warnlog.match textMuted(formatDaysAgo(item.timestamp) .. ' | ' .. item.reason) elseif #state.warnlog.status > 0 then textMuted(state.warnlog.status) else textMuted('проверка не запускалась') end local targetReason = makeReason(rule, variant) textMuted('ищу именно: ' .. targetReason) imgui.EndChild() end local function drawReferencePanel(rule, variant) local panelHeight = 126 if state.mode == 'punish' then panelHeight = 132 if variant and variant.complex then panelHeight = 152 end end imgui.BeginChild('reference_panel', imgui.ImVec2(0, panelHeight), true, imgui.WindowFlags.NoScrollbar) textAccent(state.mode == 'punish' and 'Итоговая строка' or 'Сводка') if not variant then imgui.PushStyleColor(imgui.Col.ChildBg, colors.panelDeep) imgui.BeginChild('summary_box', imgui.ImVec2(0, 42), true) if rule.punishment and #rule.punishment > 0 then textWrappedRaw('Наказание в правиле: ' .. rule.punishment) else textWrappedRaw('У пункта нет отдельной строки наказания') end imgui.EndChild() imgui.PopStyleColor() if state.mode == 'punish' then textColor(colors.warning, 'Автоматическая выдача недоступна: команда не распознана из правила.') else textMuted('Справочник: можно посмотреть пункт, но команда наказания не собирается.') end imgui.EndChild() return end local info = typeInfo[variant.punishType] if state.mode == 'punish' then if #state.commandLine > 0 then imgui.PushStyleColor(imgui.Col.ChildBg, colors.panelDeep) imgui.BeginChild('summary_box', imgui.ImVec2(0, 42), true) textWrappedRaw(state.commandLine) imgui.EndChild() imgui.PopStyleColor() else textMuted('Команда появится после выбора правила и срока') end else local summary if variant.punishType == 'sbanip' or variant.punishType == 'warn' then summary = string.format('%s: %s', info.title, makeReason(rule, variant)) else summary = string.format('%s на %s %s: %s', info.title, variant.time, info.unit, makeReason(rule, variant)) end imgui.PushStyleColor(imgui.Col.ChildBg, colors.panelDeep) imgui.BeginChild('summary_box', imgui.ImVec2(0, 42), true) textWrappedRaw(summary) imgui.EndChild() imgui.PopStyleColor() end if variant and variant.complex then textColor(colors.warning, 'В правиле есть дополнительное действие. Проверь его вручную.') elseif state.mode == 'reference' then textMuted('Режим справочника: скрипт ничего не отправляет и не вставляет в чат.') end if state.mode == 'punish' then if imgui.Button('Вставить в чат', imgui.ImVec2(140, 32)) then putCommandToChat() end imgui.SameLine() if imgui.Button('Скопировать', imgui.ImVec2(125, 32)) then copyCommand() end imgui.SameLine() local sendLabel = state.confirmArmed and 'Выдать сейчас' or 'Проверил, готово' local sendColor = state.confirmArmed and colors.red or colors.green imgui.PushStyleColor(imgui.Col.Button, sendColor) imgui.PushStyleColor(imgui.Col.ButtonHovered, sendColor) imgui.PushStyleColor(imgui.Col.ButtonActive, sendColor) if imgui.Button(sendLabel, imgui.ImVec2(155, 32)) then if state.confirmArmed then issueCommand() else state.confirmArmed = true state.lastAction = 'Проверь строку еще раз и нажми выдачу' end end imgui.PopStyleColor(3) end if #state.lastAction > 0 then imgui.SameLine() textMuted(state.lastAction) end imgui.EndChild() end local function drawDetails() imgui.BeginChild('details_panel', imgui.ImVec2(0, 0), true) local match = selectedMatch() local rule = selectedRule() local variant = selectedVariant() if not rule or not match then textWrappedRaw(state.error ~= '' and state.error or 'Выбери найденное правило слева.') imgui.EndChild() return end textAccent('Карточка правила') imgui.SameLine() chip(' Совпадение ' .. tostring(match.confidence) .. '% ', colors.accentSoft) imgui.Separator() if state.mode == 'punish' then metric('Цель:', state.nick) end metric('Пункт:', 'ПП ' .. rule.rule) metric('Раздел:', rule.section) imgui.Spacing() textWrappedRaw(rule.title) imgui.Spacing() textMuted('Наказание из правил') if rule.punishment and #rule.punishment > 0 then textWrappedRaw(rule.punishment) else textMuted('Не указано') end if rule.notes and #rule.notes > 0 then imgui.Spacing() textMuted('Примечания, исключения и примеры') for _, note in ipairs(rule.notes) do textWrappedRaw('- ' .. note) end end imgui.Separator() textAccent('Срок и тип наказания') if #rule.variants > 0 then for i, item in ipairs(rule.variants) do drawVariantButton(i, item, i == state.selectedVariant) end imgui.Spacing() local info = typeInfo[variant.punishType] chip(' тип: ' .. info.title .. ' ', info.color) imgui.SameLine() if variant.punishType ~= 'sbanip' and variant.punishType ~= 'warn' then chip(' время: ' .. variant.time .. ' ' .. info.unit .. ' ', colors.accentSoft) imgui.SameLine() end chip(' причина: ' .. makeReason(rule, variant) .. ' ', colors.panel3) imgui.Spacing() drawWarnlogHint(rule, variant) else textMuted('В этом пункте нет автоматической команды для jail/mute/ban/warn.') end imgui.Spacing() drawReferencePanel(rule, variant) imgui.EndChild() end local function drawUi() pushTheme() if imgui.IsKeyPressed(imgui.Key.Escape, false) then ui.open[0] = false state.confirmArmed = false state.warnlog.waiting = false end if ui.centerNext then local io = imgui.GetIO() local pos = imgui.ImVec2( (io.DisplaySize.x - CONFIG.windowWidth) * 0.5, (io.DisplaySize.y - CONFIG.windowHeight) * 0.5 ) imgui.SetNextWindowPos(pos, imgui.Cond.Always) ui.centerNext = false end imgui.SetNextWindowSize(imgui.ImVec2(CONFIG.windowWidth, CONFIG.windowHeight), imgui.Cond.FirstUseEver) if imgui.Begin('Хулиганы', ui.open, imgui.WindowFlags.NoCollapse) then imgui.Columns(3, 'main_columns', false) imgui.SetColumnWidth(0, 182) imgui.SetColumnWidth(1, 520) drawSidebar() imgui.NextColumn() drawHeader() imgui.Spacing() drawMatches() imgui.NextColumn() drawDetails() imgui.Columns(1) end imgui.End() popTheme() end imgui.OnFrame( function() return ui.open[0] end, drawUi ) function main() repeat wait(100) until isSampAvailable() local ok, err = loadRules() if ok then chat('Скрипт загружен. Правил: ' .. tostring(state.loadedRules) .. '. Команда: /' .. CONFIG.command) logLine('Loaded rules: ' .. tostring(state.loadedRules) .. ', skipped: ' .. tostring(state.skippedRules)) else chat(err or 'Ошибка загрузки правил', 0xFF7777FF) logLine('Load error: ' .. tostring(err)) end sampRegisterChatCommand(CONFIG.command, function(params) if #rules == 0 then local loaded, loadErr = loadRules() if not loaded then chat(loadErr or 'База правил не загрузилась', 0xFF7777FF) return end end openPunish(params) end) sampRegisterChatCommand('nks', function(params) if #rules == 0 then local loaded, loadErr = loadRules() if not loaded then chat(loadErr or 'База правил не загрузилась', 0xFF7777FF) return end end sendQuickReference(params) end) while true do wait(0) if state.warnlog.waiting and os.time() - state.warnlog.startedAt >= CONFIG.warnlogTimeout then state.warnlog.waiting = false state.warnlog.status = 'warnlog не ответил' end end end