script_name('RMarket Price Search') script_version('1.0') require 'lib.moonloader' local imgui = require 'mimgui' local encoding = require 'encoding' local json = require 'cjson' local ffi = require 'ffi' encoding.default = 'CP1251' local u8 = encoding.UTF8 local new = imgui.new local CONFIG = { command = 'rsell', fallbackCommand = 'rprices', baseUrl = 'http://103.102.228.105:3030/api/get_prices?server=', timeout = 20, windowWidth = 850, windowHeight = 430, maxMatchesPerServer = 25 } local SERVERS = { { id = 'Central', name = 'Центральный' }, { id = 'Southern', name = 'Южный' }, { id = 'Northern', name = 'Северный' }, { id = 'Eastern', name = 'Восточный' }, { id = 'Western', name = 'Западный' }, { id = 'Primorsky', name = 'Приморский' }, { id = 'Federal', name = 'Федеральный' } } local requestsOk, requests = pcall(require, 'requests') local ui = { open = new.bool(false), centerNext = false } local state = { queryInput = new.char[256](), query = '', loading = false, status = 'Введите предмет и нажмите поиск', results = {}, cache = {}, lastSearchAt = 0 } 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), 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), 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 function chat(text, color) sampAddChatMessage(u8:decode('[RM Prices] ' .. tostring(text)), color or 0x7FD6FFFF) end local function trim(text) return (text or ''):gsub('^%s+', ''):gsub('%s+$', '') end local function lowerRu(text) text = tostring(text or ''):lower() for upper, lower in pairs(ruLowerMap) do text = text:gsub(upper, lower) end return text end local function normalize(text) text = lowerRu(text or '') text = text:gsub('{%x%x%x%x%x%x}', '') 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 urlEncode(text) text = tostring(text or '') text = text:gsub('\n', '\r\n') text = text:gsub('([^%w%-_%.~])', function(char) return string.format('%%%02X', char:byte()) end) return text end local function setInput(text) text = tostring(text or '') local size = ffi.sizeof(state.queryInput) ffi.fill(state.queryInput, size) ffi.copy(state.queryInput, text, math.min(#text, size - 1)) end local function formatMoney(value) local num = tonumber(value) or 0 if num <= 0 then return '-' end local sign = '' if num < 0 then sign = '-' num = math.abs(num) end local text = tostring(math.floor(num)) while true do local replaced text, replaced = text:gsub('^(-?%d+)(%d%d%d)', '%1.%2') if replaced == 0 then break end end return sign .. text .. ' $' end local function scoreName(name, query, queryNorm, queryWords) local nameNorm = normalize(name) if name == query then return 120 end if nameNorm == queryNorm then return 110 end if nameNorm:find(queryNorm, 1, true) then return 95 end if queryNorm:find(nameNorm, 1, true) then return 80 end local matched = 0 for _, word in ipairs(queryWords) do if nameNorm:find(word, 1, true) then matched = matched + 1 end end if #queryWords == 0 then return 0 end return math.floor((matched / #queryWords) * 75) end local function findMatchingItems(data, query) if type(data) ~= 'table' then return nil end local matches = {} local seen = {} local exact = data[query] if type(exact) == 'table' then table.insert(matches, { name = query, info = exact, score = 120 }) seen[query] = true end local queryNorm = normalize(query) if queryNorm == '' then return matches end local queryWords = splitWords(query) for name, info in pairs(data) do if type(name) == 'string' and type(info) == 'table' and not seen[name] then local score = scoreName(name, query, queryNorm, queryWords) if score >= 40 then table.insert(matches, { name = name, info = info, score = score }) end end end table.sort(matches, function(a, b) if a.score ~= b.score then return a.score > b.score end local aSell = tonumber(a.info.s) or 0 local bSell = tonumber(b.info.s) or 0 if aSell ~= bSell then return aSell > bSell end return tostring(a.name) < tostring(b.name) end) while #matches > CONFIG.maxMatchesPerServer do table.remove(matches) end return matches end local function fetchServer(server) if state.cache[server.id] then return true, state.cache[server.id] end if not requestsOk then return false, 'Не найдена библиотека requests' end local url = CONFIG.baseUrl .. urlEncode(server.id) local ok, response = pcall(requests.request, 'GET', url, { timeout = CONFIG.timeout, headers = { ['User-Agent'] = 'RM-Price-Search/1.0', ['Connection'] = 'close' } }) if not ok then return false, tostring(response) end if not response or tonumber(response.status_code) ~= 200 then return false, 'HTTP ' .. tostring(response and response.status_code or 'nil') end local decodedOk, data = pcall(json.decode, response.text or '{}') if not decodedOk or type(data) ~= 'table' then return false, 'Ошибка JSON' end state.cache[server.id] = data return true, data end local function runSearch(query, forceRefresh) query = trim(query or '') if query == '' then state.status = 'Введите название предмета' return end if state.loading then return end state.loading = true state.query = query state.results = {} state.status = 'Загрузка цен...' if forceRefresh then state.cache = {} end lua_thread.create(function() local foundServers = 0 local foundItems = 0 for _, server in ipairs(SERVERS) do state.status = 'Проверяю ' .. server.name .. '...' local ok, dataOrErr = fetchServer(server) if ok then local matches = findMatchingItems(dataOrErr, query) if matches and #matches > 0 then foundServers = foundServers + 1 for _, match in ipairs(matches) do local info = match.info foundItems = foundItems + 1 table.insert(state.results, { server = server, ok = true, item = match.name, sell = tonumber(info.s) or 0, buy = tonumber(info.b) or 0, volume = tonumber(info.v) or 0, history = type(info.h) == 'table' and #info.h or 0, score = match.score }) end else table.insert(state.results, { server = server, ok = true, item = nil, sell = 0, buy = 0, volume = 0, history = 0, score = 0 }) end else table.insert(state.results, { server = server, ok = false, error = tostring(dataOrErr) }) end wait(0) end state.lastSearchAt = os.time() if foundItems > 0 then state.status = string.format('Готово. Позиций: %d, серверов: %d', foundItems, foundServers) else state.status = 'Готово. Предмет не найден' end state.loading = false end) end local function pushStyle() 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.panel2) imgui.PushStyleColor(imgui.Col.ButtonActive, colors.panel2) imgui.PushStyleColor(imgui.Col.Header, colors.panel3) imgui.PushStyleColor(imgui.Col.HeaderHovered, colors.panel2) imgui.PushStyleColor(imgui.Col.HeaderActive, colors.panel2) imgui.PushStyleVarFloat(imgui.StyleVar.WindowRounding, 8) imgui.PushStyleVarFloat(imgui.StyleVar.ChildRounding, 6) imgui.PushStyleVarFloat(imgui.StyleVar.FrameRounding, 5) imgui.PushStyleVarFloat(imgui.StyleVar.WindowBorderSize, 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 popStyle() imgui.PopStyleVar(7) imgui.PopStyleColor(12) end local function textColored(color, text) imgui.PushStyleColor(imgui.Col.Text, color) imgui.TextUnformatted(tostring(text or '')) imgui.PopStyleColor() end local function drawResults() imgui.BeginChild('results_panel', imgui.ImVec2(0, 0), true) imgui.Columns(6, 'price_columns', false) imgui.SetColumnWidth(0, 135) imgui.SetColumnWidth(1, 235) imgui.SetColumnWidth(2, 125) imgui.SetColumnWidth(3, 125) imgui.SetColumnWidth(4, 70) imgui.SetColumnWidth(5, 80) textColored(colors.muted, 'Сервер') imgui.NextColumn() textColored(colors.muted, 'Найдено как') imgui.NextColumn() textColored(colors.muted, 'Продажа') imgui.NextColumn() textColored(colors.muted, 'Скупка') imgui.NextColumn() textColored(colors.muted, 'v') imgui.NextColumn() textColored(colors.muted, 'История') imgui.NextColumn() imgui.Separator() if #state.results == 0 then imgui.Columns(1) imgui.Spacing() textColored(colors.muted, state.loading and 'Идет загрузка...' or 'Пока нет результатов') imgui.EndChild() return end for _, row in ipairs(state.results) do imgui.TextUnformatted(row.server.name) imgui.NextColumn() if not row.ok then textColored(colors.red, row.error or 'Ошибка') imgui.NextColumn() textColored(colors.red, '-') imgui.NextColumn() textColored(colors.red, '-') imgui.NextColumn() imgui.TextUnformatted('-') imgui.NextColumn() imgui.TextUnformatted('-') imgui.NextColumn() elseif row.item then local itemLabel = row.item if row.score and row.score < 110 then itemLabel = itemLabel .. string.format(' (%d%%)', math.min(row.score, 100)) end imgui.TextUnformatted(itemLabel) imgui.NextColumn() textColored(row.sell > 0 and colors.green or colors.muted, formatMoney(row.sell)) imgui.NextColumn() textColored(row.buy > 0 and colors.warning or colors.muted, formatMoney(row.buy)) imgui.NextColumn() imgui.TextUnformatted(tostring(row.volume or 0)) imgui.NextColumn() imgui.TextUnformatted(tostring(row.history or 0)) imgui.NextColumn() else textColored(colors.muted, 'Нет данных') imgui.NextColumn() textColored(colors.muted, '-') imgui.NextColumn() textColored(colors.muted, '-') imgui.NextColumn() imgui.TextUnformatted('-') imgui.NextColumn() imgui.TextUnformatted('-') imgui.NextColumn() end end imgui.Columns(1) imgui.EndChild() end local function drawUi() pushStyle() if ui.centerNext then local io = imgui.GetIO() local pos = imgui.ImVec2( (io.DisplaySize.x - CONFIG.windowWidth) / 2, (io.DisplaySize.y - CONFIG.windowHeight) / 2 ) imgui.SetNextWindowPos(pos, imgui.Cond.Always) ui.centerNext = false end imgui.SetNextWindowSize(imgui.ImVec2(CONFIG.windowWidth, CONFIG.windowHeight), imgui.Cond.FirstUseEver) if imgui.Begin('RMarket цены', ui.open, imgui.WindowFlags.NoCollapse) then imgui.BeginChild('top_panel', imgui.ImVec2(0, 96), true, imgui.WindowFlags.NoScrollbar) textColored(colors.text, 'Поиск цены предмета по всем серверам') textColored(colors.muted, 'Пример: Шлем Лича (Легендарный)') imgui.SetNextItemWidth(-190) imgui.InputText('##price_query', state.queryInput, ffi.sizeof(state.queryInput)) imgui.SameLine() if imgui.Button(state.loading and 'Ищу...' or 'Поиск', imgui.ImVec2(85, 32)) then runSearch(ffi.string(state.queryInput), false) end imgui.SameLine() if imgui.Button('Обновить', imgui.ImVec2(85, 32)) then runSearch(ffi.string(state.queryInput), true) end textColored(state.loading and colors.warning or colors.muted, state.status) imgui.EndChild() drawResults() end imgui.End() popStyle() end imgui.OnFrame( function() return ui.open[0] end, drawUi ) local function openSearch(params) ui.open[0] = true ui.centerNext = true params = trim(params or '') if params ~= '' then setInput(params) runSearch(params, false) end end function main() repeat wait(100) until isSampAvailable() if requestsOk then chat('Загружен. Команды: /' .. CONFIG.command .. ' или /' .. CONFIG.fallbackCommand) else chat('Загружен, но нет библиотеки requests. HTTP поиск не заработает.', 0xFF7777FF) end local okNk = pcall(sampRegisterChatCommand, CONFIG.command, openSearch) if not okNk then chat('Команда /' .. CONFIG.command .. ' уже занята. Используй /' .. CONFIG.fallbackCommand, 0xFFCC66FF) end if CONFIG.fallbackCommand and CONFIG.fallbackCommand ~= CONFIG.command then pcall(sampRegisterChatCommand, CONFIG.fallbackCommand, openSearch) end while true do wait(0) end end