script_name('Warnlog to TXT') script_version('1.3') require 'lib.moonloader' local sampev = require 'lib.samp.events' local CONFIG = { command = 'nakaz', warnlogCommand = '/warnlog_pl', firstId = 1, lastId = 600, minPunishments = 7, daysBack = 7, outputPath = 'D:/nakaz.txt', logPath = 'D:/nakaz_log.txt', requestDelay = 120, closeDelay = 20, dialogTimeout = 1 } local scan = { active = false, waitingDialog = false, dialogOpened = false, dialogId = -1, firstId = CONFIG.firstId, lastId = CONFIG.lastId, currentId = 0, startedAt = 0, foundCount = 0, checkedCount = 0 } local function bytes(...) return string.char(...) end local 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), igroka = bytes(232, 227, 240, 238, 234, 224), igroku = bytes(232, 227, 240, 238, 234, 243), na = bytes(237, 224), v = bytes(226), line1 = bytes(49, 46, 32, 200, 227, 240, 238, 226, 238, 233, 32, 237, 232, 234, 237, 229, 233, 236, 58, 32, 76, 111, 115, 115, 121, 95, 68, 111, 114, 109, 101, 115, 115, 46), line2 = bytes(50, 46, 32, 196, 238, 235, 230, 237, 238, 241, 242, 252, 58, 32, 51, 108, 118, 108, 44, 32, 195, 209, 32, 204, 224, 239, 239, 232, 237, 227, 224, 46), line3 = bytes(51, 46, 32, 205, 232, 234, 237, 229, 233, 236, 32, 232, 227, 240, 238, 234, 224, 32, 232, 32, 85, 73, 68, 58, 32, 37, 115, 44, 32, 85, 73, 68, 58, 37, 115, 46), line4 = bytes(52, 46, 32, 196, 238, 234, 224, 231, 224, 242, 229, 235, 252, 241, 242, 226, 224, 32, 237, 224, 240, 243, 248, 229, 237, 232, 233, 32, 40, 47, 119, 97, 114, 110, 108, 111, 103, 44, 32, 232, 237, 251, 229, 32, 228, 238, 234, 224, 231, 224, 242, 229, 235, 252, 241, 242, 226, 224, 41, 58, 32, 47, 119, 97, 114, 110, 108, 111, 103, 95, 112, 108, 32, 37, 115, 46), line5 = bytes(53, 46, 32, 207, 240, 238, 247, 224, 255, 32, 232, 237, 244, 238, 240, 236, 224, 246, 232, 255, 32, 40, 238, 239, 232, 241, 224, 237, 232, 229, 32, 232, 32, 228, 224, 242, 224, 32, 234, 224, 230, 228, 238, 227, 238, 32, 237, 224, 240, 243, 248, 229, 237, 232, 255, 41, 58), punishHeader = bytes(209, 239, 232, 241, 238, 234, 32, 237, 224, 234, 224, 231, 224, 237, 232, 233, 58, 32, 209, 32, 37, 115, 32, 239, 238, 32, 37, 115, 46) } local function chat(text, color) sampAddChatMessage('[NakazScan] ' .. text, color or 0x7FD6FFFF) end local function log(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'), text)) file:close() end local function pad2(value) value = tonumber(value) or 0 return value < 10 and ('0' .. value) or tostring(value) end local function formatDate(timestamp) local d = os.date('*t', timestamp) return string.format('%s.%s.%04d', pad2(d.day), pad2(d.month), d.year) end local function formatTime(timestamp) local d = os.date('*t', timestamp) return string.format('%s:%s', pad2(d.hour), pad2(d.min)) end local function startOfToday() local d = os.date('*t') d.hour, d.min, d.sec = 0, 0, 0 return os.time(d) end local function getPeriod() local todayStart = startOfToday() local fromTime = todayStart - CONFIG.daysBack * 86400 local toTime = todayStart + 86400 - 1 return fromTime, toTime end local function containsIgnoredAction(line) return line:find(RU.razglushil, 1, true) or line:find(RU.Razglushil, 1, true) or line:find(RU.razbanil, 1, true) or line:find(RU.Razbanil, 1, true) or line:find(RU.snyalZaglushku, 1, true) or line:find(RU.SnyalZaglushku, 1, true) or line:find(RU.vypustil, 1, true) or line:find(RU.Vypustil, 1, true) end local function containsIgnoredReason(reason) return reason:find(RU.vernu, 1, true) or reason:find(RU.Vernu, 1, true) or reason:find(RU.naVremya, 1, true) or reason:find(RU.NaVremya, 1, true) or reason:find(RU.vypustil, 1, true) or reason:find(RU.Vypustil, 1, true) or reason:find(RU.oshibka, 1, true) or reason:find(RU.Oshibka, 1, true) end local function parseDateTime(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 parseReason(line) local _, endPos = line:find(RU.prichina, 1, true) if not endPos then _, endPos = line:find(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 parsePlayerName(line) return line:match(RU.igroka .. '%s+(.+)%s+' .. RU.na .. '%s+') or line:match(RU.igroka .. '%s+(.+)%s+' .. RU.v .. '%s+') or line:match(RU.igroku .. '%s+(.+)%s+' .. RU.na .. '%s+') end local function getCheckNick(playerName) if not playerName or #playerName == 0 then return 'Nick_Name' end local nick = playerName:match('^([^%(]+)') or playerName nick = nick:gsub('^%s+', ''):gsub('%s+$', '') return #nick > 0 and nick or 'Nick_Name' end local function parseTitleInfo(title) title = title or '' local nick, uid = title:match('([^%s]+)%s+%[%s*(%d+)%s*%]') if nick and uid then return nick, uid end return nil, nil end local function parsePunishments(dialogText) local fromTime, toTime = getPeriod() local items = {} local playerName = nil for rawLine in dialogText:gmatch('[^\r\n]+') do local line = rawLine:gsub('{%x%x%x%x%x%x}', '') if not containsIgnoredAction(line) then local timestamp = parseDateTime(line) local reason = parseReason(line) if timestamp and reason and #reason > 0 and not containsIgnoredReason(reason) and timestamp >= fromTime and timestamp <= toTime then table.insert(items, { reason = reason, date = formatDate(timestamp), time = formatTime(timestamp), timestamp = timestamp }) if not playerName then playerName = parsePlayerName(line) end end end end table.sort(items, function(a, b) return a.timestamp < b.timestamp end) return items, playerName end local function appendPunishments(playerId, playerName, uid, items) local fromTime, toTime = getPeriod() local file = io.open(CONFIG.outputPath, 'a') if not file then chat('Can not open output file: ' .. CONFIG.outputPath, 0xFF5555FF) log('ERROR: can not open output file') return false end local checkNick = getCheckNick(playerName) uid = uid or 'UID' file:write(RU.line1 .. '\n') file:write(RU.line2 .. '\n') file:write(string.format(RU.line3, checkNick, uid) .. '\n') file:write(string.format(RU.line4, checkNick) .. '\n') file:write(RU.line5 .. '\n') file:write(string.format(RU.punishHeader, formatDate(fromTime), formatDate(toTime)) .. '\n\n') for index, item in ipairs(items) do file:write(string.format('%d. %s - %s (%s).\n', index, item.reason, item.date, item.time)) end file:write('\n------------------------------\n\n') file:close() return true end local function finishCurrentDialog(dialogTitle, dialogText) local items, playerName = parsePunishments(dialogText or '') local titleNick, uid = parseTitleInfo(dialogTitle) playerName = titleNick or playerName log(string.format('ID %d: dialog parsed, punishments in period: %d', scan.currentId, #items)) if #items >= CONFIG.minPunishments then if appendPunishments(scan.currentId, playerName, uid, items) then scan.foundCount = scan.foundCount + 1 chat(string.format('ID %d: found %d punishments, saved to TXT.', scan.currentId, #items)) log(string.format('ID %d: saved to output', scan.currentId)) end end end local function resetDialogState() scan.waitingDialog = false scan.dialogOpened = false scan.dialogId = -1 end local function completeCurrentId() scan.checkedCount = scan.checkedCount + 1 if scan.checkedCount % 10 == 0 then chat(string.format('Checked %d players. Current ID: %d/%d.', scan.checkedCount, scan.currentId, scan.lastId)) end resetDialogState() end local function stopScan(message) log(string.format('Scan stopped at ID %d. Checked: %d. Found: %d', scan.currentId, scan.checkedCount, scan.foundCount)) scan.active = false resetDialogState() scan.currentId = 0 if message then chat(message) end end function sampev.onShowDialog(dialogId, style, title, button1, button2, text) if scan.active and scan.waitingDialog then scan.dialogId = dialogId scan.dialogOpened = true finishCurrentDialog(title, text) lua_thread.create(function() wait(CONFIG.closeDelay) pcall(sampCloseCurrentDialogWithButton, 0) log(string.format('ID %d: dialog closed automatically', scan.currentId)) completeCurrentId() end) end end local function parseRange(params) local numbers = {} params = params or '' for value in params:gmatch('%d+') do table.insert(numbers, tonumber(value)) end if #numbers == 1 then return CONFIG.firstId, numbers[1] elseif #numbers >= 2 then return numbers[1], numbers[2] end return CONFIG.firstId, CONFIG.lastId end function main() repeat wait(100) until isSampAvailable() sampRegisterChatCommand(CONFIG.command, function(params) if scan.active then stopScan('Scan stopped manually.') return end local firstId, lastId = parseRange(params) if firstId > lastId then firstId, lastId = lastId, firstId end firstId = math.max(1, firstId) lastId = math.max(firstId, lastId) local file = io.open(CONFIG.outputPath, 'a') if file then file:write(string.format('\n========== Scan started: %s | ID %d-%d ==========\n\n', os.date('%Y-%m-%d %H:%M:%S'), firstId, lastId)) file:close() else chat('Can not create output file: ' .. CONFIG.outputPath, 0xFF5555FF) return end local logFile = io.open(CONFIG.logPath, 'w') if logFile then logFile:write(string.format('[%s] Scan started\n', os.date('%Y-%m-%d %H:%M:%S'))) logFile:close() else chat('Can not create log file: ' .. CONFIG.logPath, 0xFF5555FF) return end scan.active = true resetDialogState() scan.firstId = firstId scan.lastId = lastId scan.currentId = scan.firstId - 1 scan.foundCount = 0 scan.checkedCount = 0 chat(string.format( 'Start. Checking ID %d-%d, period: %s - %s. Use /%s again to stop.', scan.firstId, scan.lastId, formatDate(select(1, getPeriod())), formatDate(select(2, getPeriod())), CONFIG.command )) log(string.format('Period: %s - %s', formatDate(select(1, getPeriod())), formatDate(select(2, getPeriod())))) end) while true do wait(0) if scan.active then if not scan.waitingDialog then scan.currentId = scan.currentId + 1 if scan.currentId > scan.lastId then stopScan('Done. Players with 7+ punishments: ' .. scan.foundCount .. '. File: ' .. CONFIG.outputPath) else log(string.format('ID %d: send command', scan.currentId)) sampSendChat(CONFIG.warnlogCommand .. ' ' .. scan.currentId) scan.waitingDialog = true scan.dialogOpened = false scan.dialogId = -1 scan.startedAt = os.time() wait(CONFIG.requestDelay) end elseif not scan.dialogOpened and os.time() - scan.startedAt >= CONFIG.dialogTimeout then log(string.format('ID %d: no dialog, skip by timeout', scan.currentId)) completeCurrentId() end end end end