script_name('MyPame Manager') script_author('marcumypka (ну и нейронка да и что') script_version('1.2') require 'lib.moonloader' local encoding = require 'encoding' encoding.default = 'CP1251' -- поменяй, если у тебя другая кодировка игры local u8 = encoding.UTF8 -- Конвертирует ЛИТЕРАЛ, написанный в этом файле (UTF-8), в кодировку игры (CP1251). -- Использовать ТОЛЬКО для строк, которые пишу я в коде. Текст, пришедший из игры -- (ввод игрока, текст диалога от сервера), уже в нужной кодировке — через T() не пропускать. local function T(s) return u8:decode(s) end local sampev = require 'lib.samp.events' local ffi = require 'ffi' -- mimgui нужен только для всплывающего окошка-списка заготовок под настоящим -- диалогом сервера (ручной режим, без /mypame <номер>). Если библиотеки нет — -- скрипт продолжает работать как раньше, просто без этого окошка. local imgui_ok, imgui = pcall(require, 'mimgui') ffi.cdef[[ void* GetActiveWindow(); int PostMessageA(void* hWnd, unsigned int Msg, uintptr_t wParam, intptr_t lParam); ]] local WM_CHAR = 0x0102 local WM_KEYDOWN = 0x0100 local WM_KEYUP = 0x0101 local VK_BACK = 0x08 local VK_RETURN = 0x0D local function simulateType(s, delayMs) local hwnd = ffi.C.GetActiveWindow() if hwnd == nil then return false end if delayMs and delayMs > 0 then for i = 1, #s do ffi.C.PostMessageA(hwnd, WM_CHAR, s:byte(i), 0) wait(delayMs) end else for i = 1, #s do ffi.C.PostMessageA(hwnd, WM_CHAR, s:byte(i), 0) end wait(0) end return true end local function simulateBackspaceAll(maxChars, delayMs) local hwnd = ffi.C.GetActiveWindow() if hwnd == nil then return end maxChars = maxChars or 130 if delayMs and delayMs > 0 then for _ = 1, maxChars do ffi.C.PostMessageA(hwnd, WM_KEYDOWN, VK_BACK, 0) ffi.C.PostMessageA(hwnd, WM_KEYUP, VK_BACK, 0) wait(delayMs) end else for _ = 1, maxChars do ffi.C.PostMessageA(hwnd, WM_KEYDOWN, VK_BACK, 0) ffi.C.PostMessageA(hwnd, WM_KEYUP, VK_BACK, 0) end wait(0) end end local function simulateSubmit() local hwnd = ffi.C.GetActiveWindow() if hwnd == nil then return false end ffi.C.PostMessageA(hwnd, WM_KEYDOWN, VK_RETURN, 0) ffi.C.PostMessageA(hwnd, WM_KEYUP, VK_RETURN, 0) return true end ---------------------------------------------------------------- -- CONFIG ---------------------------------------------------------------- local CONFIG = { command = 'mypame', real_command = '/mypame', marker_view = 'Посмотреть', -- Общий префикс среднего пункта меню сервера. Он одинаковый и когда поле -- называется "описание", и когда "слухи" (и для любых других вариантов -- названия этого поля, если сервер их добавит) — сам текст после -- двоеточия у разных объектов разный, а "Редактировать:" — всегда одно. marker_desc = 'Редактировать:', marker_line1 = 'Редактировать первую строку', marker_line2 = 'Редактировать вторую строку', -- Что класть в буфер обмена: 'raw' (значение как есть, в CP1251 — по -- умолчанию) или 'utf8' (перекодировать CP1251 -> UTF-8 перед копированием). -- Используется только командой /mypame testclip для диагностики. clipboard_mode = 'raw', -- Пауза между символами при эмуляции печати (мс). 0 (по умолчанию) — -- "пакетный" режим, максимально быстро (см. комментарий у simulateType). -- Если игра начнёт путать/терять символы — поставь например 8 или 15, -- вернётся старый надёжный, но медленный режим "по одному символу". type_delay_ms = 0, -- Пытаться ли самому нажимать Enter после ввода текста (полностью -- без рук). Если у вас Enter в диалогах SA-MP не работает как "Далее", -- поставь false — тогда придётся нажимать "Далее" вручную, как раньше. auto_submit = true, -- Показывать ли всплывающий список заготовок под настоящим окном ввода -- сервера, когда открываешь /mypame сам (без номера). Работает только -- если у тебя есть библиотека mimgui. Переключается ещё и /mypame picker. picker_enabled = true, } for k, v in pairs(CONFIG) do if k:find('^marker_') then CONFIG[k] = T(v) end end ---------------------------------------------------------------- -- Хранение заготовок ---------------------------------------------------------------- local presetsDir = getWorkingDirectory() .. '\\config\\' local presetsFile = presetsDir .. 'mypame_presets.txt' local presets = {} local function ensureDir() if not doesDirectoryExist(presetsDir) then createDirectory(presetsDir) end end local function escapeField(s) s = s or '' return s:gsub('\\', '\\\\'):gsub('\n', '\\n'):gsub('|', '\\p') end local function unescapeField(s) return (s:gsub('\\p', '|'):gsub('\\n', '\n'):gsub('\\\\', '\\')) end -- Значения в памяти (p.desc/p.line1/p.line2) хранятся в кодировке игры (CP1251) — -- их можно напрямую слать в диалоги сервера. А вот НА ДИСКЕ файл заготовок хранится -- в UTF-8: так его можно спокойно открыть и поправить вручную в блокноте/VS Code -- без риска поломать кириллицу. Поэтому при сохранении конвертируем CP1251 -> UTF-8, -- а при загрузке — обратно UTF-8 -> CP1251. Раньше конвертации не было вообще, из-за -- чего при ручном редактировании файла (сохранённого редактором в UTF-8) значения -- на самом деле были UTF-8, но скрипт считал их CP1251 и кодировал в UTF-8 ещё раз -- перед копированием в буфer обмена — отсюда кракозябры при вставке в игру. local function toDisk(s) local ok, converted = pcall(function() return u8:encode(s or '') end) return ok and converted or (s or '') end local function fromDisk(s) local ok, converted = pcall(function() return u8:decode(s or '') end) return ok and converted or (s or '') end local function savePresets() ensureDir() local f = io.open(presetsFile, 'wb') if not f then sampAddChatMessage(T('{FF6347}[MyPame] Не удалось сохранить файл заготовок.'), -1) return end for _, p in ipairs(presets) do f:write(('%s|||%s|||%s|||%s|||%s\n'):format( escapeField(toDisk(p.desc)), escapeField(toDisk(p.line1)), escapeField(toDisk(p.line2)), escapeField(toDisk(p.descType or T('описание'))), p.favorite and '1' or '0' )) end f:close() end local function loadPresets() presets = {} ensureDir() local f = io.open(presetsFile, 'rb') if not f then return end for line in f:lines() do local desc, line1, line2, descType, favorite = line:match('^(.-)%|%|%|(.-)%|%|%|(.-)%|%|%|(.-)%|%|%|(.*)$') if not desc then -- формат без 5-го столбца (избранное появилось позже) desc, line1, line2, descType = line:match('^(.-)%|%|%|(.-)%|%|%|(.-)%|%|%|(.*)$') end if not desc then -- старый формат файла (до появления типа поля), без 4-го столбца desc, line1, line2 = line:match('^(.-)%|%|%|(.-)%|%|%|(.*)$') end if desc then table.insert(presets, { desc = fromDisk(unescapeField(desc)), line1 = fromDisk(unescapeField(line1)), line2 = fromDisk(unescapeField(line2)), descType = (descType and descType ~= '') and fromDisk(unescapeField(descType)) or T('описание'), favorite = favorite == '1', }) end end f:close() end local function stripColors(s) return (s:gsub('{%x%x%x%x%x%x}', '')) end ---------------------------------------------------------------- -- Локальный редактор — синхронный, через sampIsDialogActive/sampHasDialogRespond ---------------------------------------------------------------- local DLG_LIST = 20001 local DLG_ITEM = 20002 local DLG_INPUT_DESC = 20003 local DLG_INPUT_LINE1 = 20004 local DLG_INPUT_LINE2 = 20005 local DLG_DESC_TYPE = 20006 local DLG_CONFIRM_DELETE = 20007 local editorRunning = false -- показывает LIST-диалог и блокирует текущий поток, пока не придёт ответ local function showListBlocking(id, title, text, btn1, btn2) sampShowDialog(id, title, text, btn1, btn2, 2) while sampIsDialogActive(id) do wait(0) end local _, button, listitem = sampHasDialogRespond(id) return button, listitem end -- показывает INPUT-диалог, возвращает введённый текст или nil, если отменили local function askInput(id, title, text) sampShowDialog(id, title, text, T('Сохранить'), T('Отмена'), 1) while sampIsDialogActive(id) do wait(0) end local _, button, _, input = sampHasDialogRespond(id) if button ~= 1 then return nil end return input end -- показывает LIST-диалог для выбора типа поля (описание/слухи) local function askDescType() local text = T('Описание\nСлухи') local button, listitem = showListBlocking(DLG_DESC_TYPE, T('Тип поля'), text, T('Выбрать'), T('Отмена')) if button ~= 1 then return nil end if listitem == 0 then return T('описание') end return T('слухи') end -- показывает MSGBOX-диалог "Да/Нет", возвращает true, если нажали "Да" local function askConfirm(id, title, text) sampShowDialog(id, title, text, T('Да'), T('Нет'), 0) while sampIsDialogActive(id) do wait(0) end local _, button = sampHasDialogRespond(id) return button == 1 end local function runItemMenu(i) while true do local p = presets[i] if not p then return end -- Инфу о текущих значениях показываем в чат отдельно, а не как строки -- LIST-диалога — иначе они сами станут пунктами списка и собьют индексы -- (в SAMP style=2/LIST каждая строка text — отдельный selectable-пункт). sampAddChatMessage(T('{808080}Название: ') .. ((p.desc and p.desc ~= '') and p.desc or T('(без названия)')), -1) sampAddChatMessage(T('{808080}Первая строка: ') .. (p.line1 or ''), -1) sampAddChatMessage(T('{808080}Вторая строка: ') .. (p.line2 or ''), -1) sampAddChatMessage(T('{808080}Тип поля: ') .. ((p.descType and p.descType ~= '') and p.descType or T('описание')), -1) sampAddChatMessage(T('{808080}Избранное: ') .. (p.favorite and T('да *') or T('нет')), -1) local favLabel = p.favorite and T('Убрать из избранного') or T('Добавить в избранное') local text = T('Изменить название\nИзменить первую строку\nИзменить вторую строку\nИзменить тип (описание/слухи)\n') .. favLabel .. T('\nУдалить заготовку') local header = T('Заготовка #') .. tostring(i) if p.favorite then header = header .. T(' *') end if p.desc and p.desc ~= '' then header = header .. T(': ') .. p.desc end local button, listitem = showListBlocking(DLG_ITEM, header, text, T('Выбрать'), T('Назад')) if button ~= 1 then return end if listitem == 0 then local v = askInput(DLG_INPUT_DESC, T('Новое название'), T('Введите название заготовки:')) if v then p.desc = v; savePresets() end elseif listitem == 1 then local v = askInput(DLG_INPUT_LINE1, T('Новая первая строка'), T('Введите текст:')) if v then p.line1 = v; savePresets() end elseif listitem == 2 then local v = askInput(DLG_INPUT_LINE2, T('Новая вторая строка'), T('Введите текст:')) if v then p.line2 = v; savePresets() end elseif listitem == 3 then local v = askDescType() if v then p.descType = v; savePresets() end elseif listitem == 4 then p.favorite = not p.favorite savePresets() sampAddChatMessage(p.favorite and T('{7FFF00}[MyPame] Добавлено в избранное.') or T('{7FFF00}[MyPame] Убрано из избранного.'), -1) elseif listitem == 5 then local sure = askConfirm(DLG_CONFIRM_DELETE, T('Удалить заготовку?'), T('Вы точно хотите удалить эту заготовку? Это действие нельзя отменить.')) if sure then table.remove(presets, i) savePresets() sampAddChatMessage(T('{7FFF00}[MyPame] Заготовка удалена.'), -1) return end end end end local function runEditor() while true do -- Сначала избранные (в своём исходном порядке), потом остальные — -- порядковые номера заготовок (#i) при этом не меняются, меняется -- только то, в каком порядке они показаны в этом списке. local order = {} for i in ipairs(presets) do if presets[i].favorite then table.insert(order, i) end end for i in ipairs(presets) do if not presets[i].favorite then table.insert(order, i) end end local text = '' for _, i in ipairs(order) do local p = presets[i] local preview = (p.desc and p.desc ~= '') and p.desc or (p.line1 ~= '' and p.line1 or '') local star = p.favorite and T('* ') or '' text = text .. star .. tostring(i) .. '. ' .. preview .. '\n' end text = text .. T('+ Добавить новую заготовку') local button, listitem = showListBlocking(DLG_LIST, T('Мои заготовки /mypame'), text, T('Выбрать'), T('Закрыть')) if button ~= 1 then return end if listitem + 1 > #order then local desc = askInput(DLG_INPUT_DESC, T('Новая заготовка: название'), T('Введите название (для себя, необязательно):')) if desc ~= nil then local line1 = askInput(DLG_INPUT_LINE1, T('Новая заготовка: первая строка'), T('Введите первую строку:')) if line1 then local line2 = askInput(DLG_INPUT_LINE2, T('Новая заготовка: вторая строка'), T('Введите вторую строку:')) if line2 then table.insert(presets, { desc = desc, line1 = line1, line2 = line2, descType = T('описание'), favorite = false }) savePresets() sampAddChatMessage(T('{7FFF00}[MyPame] Заготовка #') .. tostring(#presets) .. T(' сохранена.'), -1) end end end else runItemMenu(order[listitem + 1]) end end end local automation = { active = false } local pendingRealDialog = nil local lastRawDialog = nil ---------------------------------------------------------------- -- Всплывающий список заготовок (ручной режим: сам открыл /mypame, -- сам выбрал "первую"/"вторую строку" в меню сервера — а под окошком -- ввода появляется список твоих заготовок, кликаешь — текст вставляется) ---------------------------------------------------------------- local lastMainMenuText = nil -- текст последнего показанного главного меню сервера local lastMenuDescType = nil -- какой тип поля сейчас у сервера ("описание" / "слухи"), как есть, из текста меню local pendingFieldGuess = nil -- 'line1' / 'line2' / nil — какое поле выбрали в меню local pickerSelection = nil -- значение заготовки, которое надо вставить (ставится из imgui-клика) local presetPicker = { visible = false, field = nil, dialogId = nil, descType = nil } local function presetOrderForPicker() local order = {} for i in ipairs(presets) do if presets[i].favorite then table.insert(order, i) end end for i in ipairs(presets) do if not presets[i].favorite then table.insert(order, i) end end return order end -- Сравнивает тип поля заготовки с тем, что сейчас показывает сервер. -- Если тип сервера не удалось определить (nil) — фильтр не применяется, -- показываем все заготовки, чтобы список не оказался пустым просто так. local function descTypeMatches(presetType, serverType) if not serverType or serverType == '' then return true end presetType = presetType or '' if presetType == '' then return false end return presetType:find(serverType, 1, true) ~= nil or serverType:find(presetType, 1, true) ~= nil end if imgui_ok then local pickerOpenRef = imgui.new.bool(true) imgui.OnFrame(function() return presetPicker.visible end, function() imgui.SetNextWindowSize(imgui.ImVec2(460, 360), imgui.Cond.FirstUseEver) pickerOpenRef[0] = true local fieldLabel = (presetPicker.field == 'line1') and 'первая строка' or 'вторая строка' local typeLabel = presetPicker.descType and (' \226\128\148 ' .. toDisk(presetPicker.descType)) or '' imgui.Begin('Заготовки MyPame: ' .. fieldLabel .. typeLabel, pickerOpenRef, imgui.WindowFlags.NoCollapse) if #presets == 0 then imgui.TextDisabled('Нет заготовок. Набери /mypame edit, чтобы добавить.') else local shown = 0 for _, i in ipairs(presetOrderForPicker()) do local p = presets[i] if descTypeMatches(p.descType, presetPicker.descType) then local val = presetPicker.field and p[presetPicker.field] if val and val ~= '' then shown = shown + 1 if shown > 1 then imgui.Separator() end -- Заголовок блока-заготовки: номер, звёздочка избранного, -- название (если есть) и её тип поля (описание/слухи). local nameBadge = (p.desc and p.desc ~= '') and (' \226\128\148 ' .. toDisk(p.desc)) or '' local typeBadge = ' [' .. toDisk((p.descType and p.descType ~= '') and p.descType or T('описание')) .. ']' imgui.TextDisabled((p.favorite and '* ' or '') .. '#' .. tostring(i) .. nameBadge .. typeBadge) -- Предпросмотр обеих строк заготовки: активная (та, что -- сейчас редактируется) — кликабельна, вторая — просто для контекста. local line1Preview = (p.line1 and p.line1 ~= '') and toDisk(p.line1) or nil local line2Preview = (p.line2 and p.line2 ~= '') and toDisk(p.line2) or nil if presetPicker.field == 'line1' then if line1Preview and imgui.Selectable('1: ' .. line1Preview, false) then pickerSelection = p.line1 presetPicker.visible = false end if line2Preview then imgui.TextDisabled('2: ' .. line2Preview) end else if line1Preview then imgui.TextDisabled('1: ' .. line1Preview) end if line2Preview and imgui.Selectable('2: ' .. line2Preview, false) then pickerSelection = p.line2 presetPicker.visible = false end end end end end if shown == 0 then imgui.TextDisabled('Нет заготовок с таким типом поля (описание/слухи) и непустой нужной строкой.') end end imgui.End() if not pickerOpenRef[0] then presetPicker.visible = false end end) end local function matchesServerMenu(text) local t = stripColors(text) return t:find(CONFIG.marker_view, 1, true) and t:find(CONFIG.marker_line1, 1, true) and t:find(CONFIG.marker_line2, 1, true) end local function findItemIndex(text, marker, skipDescLine) local t = stripColors(text) local i = 0 for line in (t .. '\n'):gmatch('(.-)\n') do local isDescLine = line:find(CONFIG.marker_desc, 1, true) ~= nil if not (skipDescLine and isDescLine) and marker and line:find(marker, 1, true) then return i, line end i = i + 1 end return nil end -- Ищет пункт "Редактировать: <тип>" и возвращает (индекс, что написано после -- двоеточия — т.е. текущий тип поля: "описание" или "слухи"). local function findDescTypeItem(text) local t = stripColors(text) local i = 0 for line in (t .. '\n'):gmatch('(.-)\n') do local s, e = line:find(CONFIG.marker_desc, 1, true) if s then local after = line:sub(e + 1):gsub('^%s+', ''):gsub('%s+$', '') return i, after end i = i + 1 end return nil, nil end local function waitDialogActive(id, timeoutMs) local waited = 0 while not sampIsDialogActive(id) and waited < timeoutMs do wait(0) waited = waited + 1 end return sampIsDialogActive(id) end local function respondToServerDialog(id, response, listitem, inputText) local becameActive = waitDialogActive(id, 1000) if automation.debug then local curId = sampGetCurrentDialogId and sampGetCurrentDialogId() or -1 sampAddChatMessage(T('{808080}[DBG] отправляю ответ: target_id=') .. tostring(id) .. T(' active=') .. tostring(becameActive) .. T(' current_dialog_id=') .. tostring(curId) .. T(' response=') .. tostring(response) .. T(' listitem=') .. tostring(listitem) .. T(' text="') .. tostring(inputText) .. T('"'), -1) end local ok, err = pcall(sampSendDialogResponse, id, response, listitem, inputText or '') if automation.debug and not ok then sampAddChatMessage(T('{FF6347}[DBG] sampSendDialogResponse упал с ошибкой: ') .. tostring(err), -1) end end sampev.onShowDialog = function(dialogId, style, title, button1, button2, text) -- игнорируем свои же локальные диалоги редактора if dialogId >= DLG_LIST and dialogId <= DLG_CONFIRM_DELETE then return end lastRawDialog = { id = dialogId, style = style, title = title, text = text } if not automation.active then -- Ручной режим (без /mypame <номер>): сами следим за диалогами, -- чтобы понять, когда открылось окошко ввода первой/второй строки, -- и показать под ним список заготовок. if matchesServerMenu(text) then lastMainMenuText = text local _, curType = findDescTypeItem(text) lastMenuDescType = curType presetPicker.visible = false elseif style == 1 or style == 3 then if imgui_ok and CONFIG.picker_enabled and pendingFieldGuess then presetPicker.field = pendingFieldGuess presetPicker.dialogId = dialogId presetPicker.descType = lastMenuDescType presetPicker.visible = true end else presetPicker.visible = false end return end if matchesServerMenu(text) then pendingRealDialog = { id = dialogId, style = style, text = text, isMenu = true } else pendingRealDialog = { id = dialogId, style = style, text = text, isMenu = false } end if automation.debug then sampAddChatMessage(T('{808080}[DBG] id=') .. tostring(dialogId) .. T(' style=') .. tostring(style), -1) for line in (text .. '\n'):gmatch('(.-)\n') do sampAddChatMessage(T('{808080}[DBG] > ') .. line, -1) end end end -- Ловим клик игрока по настоящему меню сервера (ручной режим), чтобы понять, -- какое поле он выбрал ("первую строку" / "вторую строку") — это нужно, -- чтобы под следующим окном ввода показать список заготовок нужного поля. sampev.onSendDialogResponse = function(dialogId, response, listitem, inputtext) if dialogId >= DLG_LIST and dialogId <= DLG_CONFIRM_DELETE then return end if automation.active then return end if response == 1 and lastMainMenuText and matchesServerMenu(lastMainMenuText) then local idx1 = findItemIndex(lastMainMenuText, CONFIG.marker_line1, true) local idx2 = findItemIndex(lastMainMenuText, CONFIG.marker_line2, true) if idx1 and listitem == idx1 then pendingFieldGuess = 'line1' elseif idx2 and listitem == idx2 then pendingFieldGuess = 'line2' else pendingFieldGuess = nil end else pendingFieldGuess = nil end end local function runAutomation(preset) automation.active = true automation.debug = false pendingRealDialog = nil sampSendChat(CONFIG.real_command) local function waitDialog(timeoutMs) local waited = 0 while not pendingRealDialog and waited < timeoutMs do wait(10) waited = waited + 10 end local d = pendingRealDialog pendingRealDialog = nil return d end local function resolveToInput(preferredMarker) for _ = 1, 5 do -- защита от зацикливания local d = waitDialog(4000) if not d then return nil end if d.style == 1 or d.style == 3 then return d -- это и есть поле ввода end if d.isMenu then return nil -- сервер почему-то вернул в главное меню — что-то пошло не так end local idx, matchedLine = nil, nil if preferredMarker then idx, matchedLine = findItemIndex(d.text, preferredMarker, true) end if not idx then idx = 0 end -- fallback: берём первый пункт, если маркер не задан/не найден if automation.debug then sampAddChatMessage(T('{808080}[DBG] выбираю пункт #') .. tostring(idx) .. T(' в подменю | ') .. tostring(matchedLine), -1) end respondToServerDialog(d.id, 1, idx, '') end return nil end -- Сервер всегда открывает /mypame с полем "описание" по умолчанию. -- Если в заготовке выбран тип "слухи" (или наоборот отличается от того, -- что сейчас показывает сервер) — сначала переключаем это поле, кликнув -- по пункту "Редактировать: ...", и только потом переходим к строкам. local wantedDescType = (preset.descType and preset.descType ~= '') and preset.descType or T('описание') do local menu = waitDialog(4000) if not menu or not menu.isMenu then sampAddChatMessage(T('{FF6347}[MyPame] Не дождался меню сервера. Набери /mypame debug и пришли, что выведет.'), -1) automation.active = false return end local idx, currentType = findDescTypeItem(menu.text) if not idx then sampAddChatMessage(T('{FF6347}[MyPame] Не нашёл пункт "Редактировать: ..." в меню сервера.'), -1) automation.active = false return end if not (currentType and currentType:find(wantedDescType, 1, true)) then if automation.debug then sampAddChatMessage(T('{808080}[DBG] переключаю тип поля: "') .. tostring(currentType) .. T('" -> "') .. wantedDescType .. T('"'), -1) end respondToServerDialog(menu.id, 1, idx, '') local after = waitDialog(4000) if not after then sampAddChatMessage(T('{FF6347}[MyPame] Не дождался ответа сервера после переключения описание/слухи.'), -1) automation.active = false return end -- Если после клика сервер попросил что-то ввести (а не сам -- переключил тип и вернул то же меню) — вводим желаемый тип. if after.style == 1 or after.style == 3 then simulateBackspaceAll(140, 0) local typed = simulateType(wantedDescType, CONFIG.type_delay_ms) if typed and CONFIG.auto_submit then wait(80) pendingRealDialog = nil simulateSubmit() after = waitDialog(4000) if not after then sampAddChatMessage(T('{FF6347}[MyPame] Не дождался перехода после переключения описание/слухи.'), -1) automation.active = false return end end end pendingRealDialog = after else pendingRealDialog = menu end end local fields = { { key = 'line1', marker = CONFIG.marker_line1 }, { key = 'line2', marker = CONFIG.marker_line2 }, } for _, field in ipairs(fields) do local value = preset[field.key] if value and value ~= '' then local menu = waitDialog(4000) if not menu or not menu.isMenu then sampAddChatMessage(T('{FF6347}[MyPame] Не дождался меню сервера. Набери /mypame debug и пришли, что выведет.'), -1) automation.active = false return end local idx, matchedLine = findItemIndex(menu.text, field.marker, true) if not idx then sampAddChatMessage(T('{FF6347}[MyPame] Не нашёл нужный пункт в меню сервера.'), -1) automation.active = false return end if automation.debug then sampAddChatMessage(T('{808080}[DBG] поле "') .. field.key .. T('" -> пункт #') .. tostring(idx) .. T(' | ') .. tostring(matchedLine), -1) end respondToServerDialog(menu.id, 1, idx, '') local input = resolveToInput(nil) if not input then sampAddChatMessage(T('{FF6347}[MyPame] Не дождался поля ввода (возможно, появилось незнакомое подменю). Набери /mypame debug сразу после зависания.'), -1) automation.active = false return end simulateBackspaceAll(140, 0) local typed = simulateType(value, CONFIG.type_delay_ms) if not typed then sampAddChatMessage(T('{FF6347}[MyPame] Не удалось получить окно игры для эмуляции ввода. Впиши вручную и нажми "Далее": ') .. value, -1) pendingRealDialog = nil local afterManual = waitDialog(120000) if not afterManual then sampAddChatMessage(T('{FF6347}[MyPame] Не дождался, что поле "') .. field.key .. T('" будет пройдено вручную. Останавливаюсь.'), -1) automation.active = false return end pendingRealDialog = afterManual elseif CONFIG.auto_submit then wait(80) pendingRealDialog = nil simulateSubmit() local afterSubmit = waitDialog(4000) if not afterSubmit then sampAddChatMessage(T('{FF6347}[MyPame] Поле "') .. field.key .. T('" введено, но авто-Enter не сработал. Нажми "Далее" вручную.'), -1) local afterManual = waitDialog(120000) if not afterManual then sampAddChatMessage(T('{FF6347}[MyPame] Не дождался перехода. Останавливаюсь.'), -1) automation.active = false return end pendingRealDialog = afterManual else pendingRealDialog = afterSubmit end else sampAddChatMessage(T('{7FFF00}[MyPame] Текст введён. Проверь поле и нажми "Далее".'), -1) pendingRealDialog = nil local afterManual = waitDialog(120000) if not afterManual then sampAddChatMessage(T('{FF6347}[MyPame] Не дождался, что поле "') .. field.key .. T('" будет пройдено вручную. Останавливаюсь.'), -1) automation.active = false return end pendingRealDialog = afterManual end end end local final = waitDialog(1500) if final then sampSendChat(CONFIG.real_command) end automation.active = false sampAddChatMessage(T('{7FFF00}[MyPame] Готово.'), -1) end ---------------------------------------------------------------- -- Команда ---------------------------------------------------------------- sampRegisterChatCommand(CONFIG.command, function(param) param = param:gsub('^%s+', ''):gsub('%s+$', '') if param == '' then sampSendChat(CONFIG.real_command) return end if param == 'testclip' then local testWord = T('тест') local clipboardValue = testWord if CONFIG.clipboard_mode == 'utf8' then local ok, converted = pcall(function() return u8:encode(testWord) end) if ok and converted then clipboardValue = converted end end local ok = pcall(setClipboardText, clipboardValue) if ok then sampAddChatMessage(T('{FFFF00}[MyPame] Режим "') .. CONFIG.clipboard_mode .. T('". В буфер скопировано слово "тест". Сверни игру, открой Блокнот и вставь туда (Ctrl+V). Напиши, что получилось.'), -1) else sampAddChatMessage(T('{FF6347}[MyPame] setClipboardText упал с ошибкой.'), -1) end return end if param:find('^hex') then local n = tonumber(param:match('%d+')) if not n or not presets[n] then sampAddChatMessage(T('{FF6347}[MyPame] Укажи номер заготовки: /mypame hex <номер>'), -1) return end local function toHex(s) return (s or ''):gsub('.', function(c) return ('%02X '):format(c:byte()) end) end sampAddChatMessage(T('{808080}line1 hex: ') .. toHex(presets[n].line1), -1) sampAddChatMessage(T('{808080}line2 hex: ') .. toHex(presets[n].line2), -1) return end if param == 'edit' then if editorRunning then sampAddChatMessage(T('{FF6347}[MyPame] Редактор уже открыт.'), -1) return end lua_thread.create(function() editorRunning = true runEditor() editorRunning = false end) return end if param == 'picker' then if not imgui_ok then sampAddChatMessage(T('{FF6347}[MyPame] Библиотека mimgui не найдена, список заготовок недоступен.'), -1) return end CONFIG.picker_enabled = not CONFIG.picker_enabled if not CONFIG.picker_enabled then presetPicker.visible = false end sampAddChatMessage(CONFIG.picker_enabled and T('{7FFF00}[MyPame] Всплывающий список заготовок включён.') or T('{7FFF00}[MyPame] Всплывающий список заготовок выключен.'), -1) return end if param == 'debug' then if not lastRawDialog then sampAddChatMessage(T('{FF6347}[MyPame] Пока не было ни одного диалога.'), -1) return end sampAddChatMessage(T('{808080}id=') .. tostring(lastRawDialog.id) .. T(' style=') .. tostring(lastRawDialog.style), -1) for line in (lastRawDialog.text .. '\n'):gmatch('(.-)\n') do sampAddChatMessage(T('{808080}> ') .. line, -1) end return end local n = tonumber(param) if not n or not presets[n] then sampAddChatMessage(T('{FF6347}[MyPame] Заготовки с таким номером нет. /mypame edit — список.'), -1) return end if automation.active then sampAddChatMessage(T('{FF6347}[MyPame] Уже выполняется, подожди.'), -1) return end lua_thread.create(runAutomation, presets[n]) end) function main() loadPresets() while not isSampAvailable() do wait(100) end sampAddChatMessage(T('{7FFF00}[MyPame Manager] Загружено. /mypame edit — заготовки, /mypame <номер> — применить'), -1) if not imgui_ok then sampAddChatMessage(T('{FFA500}[MyPame] mimgui не найден — всплывающий список заготовок при ручном /mypame будет недоступен, остальное работает как обычно.'), -1) end -- Отдельный поток: следит за закрытием настоящего диалога (чтобы спрятать -- список заготовок) и подставляет текст, когда кликнули по заготовке в окне. lua_thread.create(function() while true do wait(0) if presetPicker.visible and presetPicker.dialogId and not sampIsDialogActive(presetPicker.dialogId) then presetPicker.visible = false end if pickerSelection then local val = pickerSelection pickerSelection = nil simulateBackspaceAll(140, 0) local typed = simulateType(val, CONFIG.type_delay_ms) if typed and CONFIG.auto_submit then wait(80) simulateSubmit() end end end end) wait(-1) end