-- Nakhodka rebuild by Codex & Ferkomen script_author('Codex & Ferkomen') script_name('nakhodka_legal') KH_LEGAL_BUILD = true NK_BOOT_BASE_URL = 'http://138.124.127.115:27816/files/' NK_SCRIPT_VERSION = 2026062002 NK_SCRIPT_VERSION_TEXT = '2026.06.20.2' NK_UPDATE_SCRIPT_URLS = { 'https://nakhodka.fun/NakhodkaLegal.lua', 'https://www.nakhodka.fun/NakhodkaLegal.lua' } NK_UPDATE_MIN_SIZE = 90000 NK_BOOT_SEP = string.char(92) function nkBootWorkDir() if type(getWorkingDirectory) == 'function' then local ok, dir = pcall(getWorkingDirectory) if ok and dir ~= nil and tostring(dir) ~= '' then return tostring(dir) end end return 'moonloader' end function nkBootJoin(base, rel) rel = tostring(rel or ''):gsub('/', NK_BOOT_SEP) if tostring(base or '') == '' then return rel end return tostring(base or '') .. NK_BOOT_SEP .. rel end function nkBootFileSize(path) local file = io.open(path, 'rb') if not file then return -1 end local size = file:seek('end') or -1 file:close() return tonumber(size) or -1 end function nkBootEnsureDirFor(path) if type(createDirectory) ~= 'function' then return end local text = tostring(path or '') local lastSlash = 0 for index = 1, #text do local ch = text:sub(index, index) if ch == '/' or ch == NK_BOOT_SEP then lastSlash = index end end if lastSlash <= 0 then return end local dir = text:sub(1, lastSlash - 1) local drive = dir:match('^%a:') local rest = dir local current = '' if drive ~= nil then current = drive rest = dir:sub(#drive + 1) while rest:sub(1, 1) == '/' or rest:sub(1, 1) == NK_BOOT_SEP do rest = rest:sub(2) end end rest = rest:gsub(NK_BOOT_SEP, '/') for part in rest:gmatch('[^/]+') do if current == '' then current = part else current = current .. NK_BOOT_SEP .. part end if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(current) then pcall(createDirectory, current) end end end function nkBootDownload(rel, path, minSize) return nkBootDownloadUrl(NK_BOOT_BASE_URL .. tostring(rel or ''):gsub(NK_BOOT_SEP, '/'), path, minSize) end function nkBootDownloadUrl(url, path, minSize) minSize = tonumber(minSize) or 1 if type(downloadUrlToFile) ~= 'function' then return false end nkBootEnsureDirFor(path) url = tostring(url or '') if url == '' then return false end local done, ok = false, false local endStatus = 6 pcall(function() local ml = require 'moonloader' if ml ~= nil and ml.download_status ~= nil and ml.download_status.STATUS_ENDDOWNLOADDATA ~= nil then endStatus = ml.download_status.STATUS_ENDDOWNLOADDATA end end) local started = os.clock() local lastSize, stableAt = -2, os.clock() local startedOk = pcall(downloadUrlToFile, url, path, function(_, status) if status == endStatus then done, ok = true, true end end) if not startedOk then return false end while not done and os.clock() - started < 45 do local size = nkBootFileSize(path) if size >= minSize and size == lastSize and os.clock() - stableAt > 0.65 then done, ok = true, true break end if size ~= lastSize then lastSize, stableAt = size, os.clock() end if type(wait) == 'function' then wait(0) else break end end return ok and nkBootFileSize(path) >= minSize end function nkBootReadAll(path) local file = io.open(path, 'rb') if not file then return nil end local data = file:read('*a') file:close() return data end function nkBootWriteAll(path, data) nkBootEnsureDirFor(path) local file = io.open(path, 'wb') if not file then return false end file:write(tostring(data or '')) file:close() return true end function nkBootSelfPath() local ok, info = pcall(debug.getinfo, 1, 'S') local source = ok and info and tostring(info.source or '') or '' if source:sub(1, 1) == '@' and source:sub(2) ~= '' then return source:sub(2) end return nkBootJoin(nkBootWorkDir(), 'NakhodkaLegal.lua') end function nkBootBackupPath(path) return tostring(path or '') .. '.bak-update-' .. os.date('%Y%m%d-%H%M%S') end function nkExtractScriptVersion(text) return tonumber(tostring(text or ''):match('NK_SCRIPT_VERSION%s*=%s*(%d+)')) or 0 end function nkBootCheckSelfUpdate() local selfPath = nkBootSelfPath() local tempPath = nkBootJoin(nkBootWorkDir(), 'config/nakhodka_legal_update.tmp') local urls = NK_UPDATE_SCRIPT_URLS or {} local downloaded = false for _, baseUrl in ipairs(urls) do local sep = tostring(baseUrl):find('?', 1, true) and '&' or '?' if nkBootDownloadUrl(tostring(baseUrl) .. sep .. 't=' .. tostring(os.time()), tempPath, NK_UPDATE_MIN_SIZE or 1) then downloaded = true break end end if not downloaded then return false, false, 'download' end local remoteText = nkBootReadAll(tempPath) local localText = nkBootReadAll(selfPath) if remoteText == nil or #remoteText < (NK_UPDATE_MIN_SIZE or 1) then return false, false, 'empty' end if not remoteText:find('NK_SCRIPT_VERSION', 1, true) or not remoteText:find("script_name('nakhodka_legal')", 1, true) then return false, false, 'bad_file' end if localText ~= nil and remoteText == localText then return true, false, 'same' end local remoteVersion = nkExtractScriptVersion(remoteText) local localVersion = tonumber(NK_SCRIPT_VERSION) or nkExtractScriptVersion(localText) if remoteVersion > 0 and localVersion > 0 and remoteVersion < localVersion then return true, false, 'older' end if localText ~= nil then nkBootWriteAll(nkBootBackupPath(selfPath), localText) end if not nkBootWriteAll(selfPath, remoteText) then return false, false, 'write' end return true, true, 'updated' end function nkBootReadManifest(path) local items = {} local file = io.open(path, 'rb') if not file then return items end for line in file:lines() do line = tostring(line or ''):gsub('^%s+', ''):gsub('%s+$', '') if line ~= '' and line:sub(1, 1) ~= '#' then local rel, minSize = line:match('^([^|]+)|(%d+)$') if rel ~= nil then items[#items + 1] = {rel = rel:gsub(NK_BOOT_SEP, '/'), minSize = tonumber(minSize) or 1} end end end file:close() return items end function nkBootstrapFiles() local base = nkBootWorkDir() local manifestPath = nkBootJoin(base, 'config/nakhodka_manifest.txt') nkBootDownload('manifest.txt', manifestPath, 10) local items = nkBootReadManifest(manifestPath) if #items == 0 then return 0 end local downloaded, failed = 0, 0 for _, item in ipairs(items) do local path = nkBootJoin(base, item.rel) if nkBootFileSize(path) < item.minSize then if nkBootDownload(item.rel, path, item.minSize) then downloaded = downloaded + 1 else failed = failed + 1 end end end if downloaded > 0 then print(string.format('Nakhodka bootstrap: downloaded %d file(s).', downloaded)) end if failed > 0 then print(string.format('Nakhodka bootstrap: failed %d file(s).', failed)) end return downloaded end pcall(nkBootstrapFiles) local sampev = require 'lib.samp.events' local inicfg = require 'inicfg' -- \xcf\xee\xe4\xea\xeb\xfe\xf7\xe0\xe5\xec \xe1\xe8\xe1\xeb\xe8\xee\xf2\xe5\xea\xf3 \xe4\xeb\xff \xf1\xee\xf5\xf0\xe0\xed\xe5\xed\xe8\xff \xea\xee\xed\xf4\xe8\xe3\xe0 local encoding = require 'encoding' encoding.default = 'CP1251' local u8 = encoding.UTF8 local ffi = require 'ffi' local khBitReady, khBit = pcall(require, 'bit') if not khBitReady then khBit = nil end if KH_LEGAL_BUILD then khSocketReady, khSocket = false, nil else khSocketReady, khSocket = pcall(require, 'socket') end khJsonReady, khJson = pcall(require, 'cjson') if not khJsonReady then khJsonReady, khJson = pcall(require, 'dkjson') end khRequestsReady, khRequests = pcall(require, 'requests') khEffilReady, khEffil = pcall(require, 'effil') local imguiReady, imgui = pcall(require, 'mimgui') if not imguiReady then imgui = nil end khBlurReady, khBlur = pcall(require, 'mimgui_blur') if not khBlurReady then khBlur = nil end khParticlesReady, khParticlesLib = pcall(require, 'Particles') if not khParticlesReady then khParticlesLib = nil end khBlurSkipFrames = 0 if khBlur ~= nil and type(addEventHandler) == 'function' then pcall(addEventHandler, 'onD3DDeviceLost', function() khBlurSkipFrames = 120 end) end khUiFontLoaded = false khBiFontLoaded = false khBiGlyphRanges = nil if imguiReady and imgui ~= nil and imgui.new ~= nil and imgui.OnInitialize ~= nil then khBiGlyphRanges = imgui.new.ImWchar[3](0xF000, 0xF8FF, 0) imgui.OnInitialize(function() local io = imgui.GetIO() local fonts = io and io.Fonts or nil if fonts ~= nil then local mainCfg = imgui.ImFontConfig() mainCfg.PixelSnapH = true mainCfg.OversampleH = 3 mainCfg.OversampleV = 2 local ranges = nil if fonts.GetGlyphRangesCyrillic ~= nil then ranges = fonts:GetGlyphRangesCyrillic() end local okMain, mainFont = pcall(function() return fonts:AddFontFromFileTTF('C:\\Windows\\Fonts\\segoeuib.ttf', 16.5, mainCfg, ranges) end) khUiFontLoaded = okMain and mainFont ~= nil if khUiFontLoaded then pcall(function() io.FontDefault = mainFont end) end local cfg = imgui.ImFontConfig() cfg.MergeMode = true cfg.PixelSnapH = true local ok, font = pcall(function() return fonts:AddFontFromFileTTF('moonloader/resource/fonts/bootstrap-icons.ttf', 16.0, cfg, khBiGlyphRanges) end) khBiFontLoaded = ok and font ~= nil end end) end local new = imguiReady and imgui.new or nil local khMenuState = imguiReady and new.bool(false) or nil khMenuBlurEnabled = imguiReady and new.bool(false) or {[0] = false} khMenuBlurRadius = imguiReady and new.int(8) or {[0] = 8} khParticlesEnabled = imguiReady and new.bool(false) or {[0] = false} khFilesChecking = false khAutoUpdateEnabled = imguiReady and new.bool(true) or {[0] = true} khUpdateChecking = false local khActiveTab = 1 local khTargetTab = 1 local khContentAlpha = 1.0 local khScriptEnabled = imguiReady and new.bool(true) or {[0] = true} local khOnlyInZone = imguiReady and new.bool(false) or {[0] = false} khZoneOpsEnabled = imguiReady and new.bool(true) or {[0] = true} local khDopDefaultIcon = imguiReady and new.int(14) or {[0] = 14} local khDopShowBlips = imguiReady and new.bool(true) or {[0] = true} local khPointDisplayRadius = imguiReady and new.int(300) or {[0] = 300} local khPointCheckRadius = imguiReady and new.int(12) or {[0] = 12} local khPointIcon = imguiReady and new.int(56) or {[0] = 56} kh3DMarkersEnabled = imguiReady and new.bool(false) or {[0] = false} kh3DMarkersAntiWh = imguiReady and new.bool(false) or {[0] = false} kh3DMarkerType = imguiReady and new.int(0) or {[0] = 0} kh3DMarkerDistance = imguiReady and new.int(100) or {[0] = 100} kh3DArrowDistance = imguiReady and new.int(300) or {[0] = 300} kh3DMarkerRadius10 = imguiReady and ffi.new('float[1]', 1.5) or {[0] = 1.5} kh3DNormalR = imguiReady and new.int(255) or {[0] = 255} kh3DNormalG = imguiReady and new.int(255) or {[0] = 255} kh3DNormalB = imguiReady and new.int(255) or {[0] = 255} kh3DNormalA = imguiReady and new.int(255) or {[0] = 255} kh3DCheckedR = imguiReady and new.int(255) or {[0] = 255} kh3DCheckedG = imguiReady and new.int(51) or {[0] = 51} kh3DCheckedB = imguiReady and new.int(51) or {[0] = 51} kh3DCheckedA = imguiReady and new.int(255) or {[0] = 255} kh3DColorTarget = 0 kh3DMainDrawList = {} kh3DDopDrawList = {} kh3DPickupMarkers = {} kh3DPickupClearQueue = {} kh3DNativePauseUntil = 0 kh3DLastBuildAt = 0 kh3DScanInterval = 0.08 kh3DRenderLineDisabled = false kh3DWorldToScreenDisabled = false kh3DActiveMarkerType = nil kh3DSwitchCooldownUntil = 0 kh3DForceClear = false kh3DClearingPickups = false kh3DSwitchPending = false kh3DPendingMarkerType = nil khClear3DPickups = function() end local khDopSelectedIndex = -1 local khDropHudEnabled = imguiReady and new.bool(true) or {[0] = true} khDropHudReady = false khDropSelectedDate = nil khDropSelectedLogId = nil local khHudMoveMode = false local khHudMoveArmed = false local khHudSaveLatch = false local khHudMoveCooldown = 0 local khHudPos = {x = 22, y = 220} khTyanEnabled = imguiReady and new.bool(false) or {[0] = false} khTyanPos = {x = 240, y = 500} khTyanSize = 260 khTyanSizeInput = imguiReady and new.int(260) or {[0] = 260} khTyanTexture = nil khTyanTextureTried = false khTyanTextures = {} khTyanTextureTriedByVariant = {} khTyanPendingVariant = nil khTyanSwitchApplyAt = 0 khTyanTextureVariant = -1 khTyanVariant = 0 khTyanText = '' khTyanLoadingDots = false khTyanLoadingStartedAt = 0 khTyanStartedAt = 0 khTyanHoldSec = 5 khTyanNextPhraseAt = 0 khTyanDragging = false khTyanMoveMode = false khTyanMoveSaveLatch = false khTyanDragOffset = {x = 0, y = 0} khTyanWasEnabled = false khTyanPhrases = {} khTyanCelebratePhrases = {} khMaratPhrases = {} khMaratCelebratePhrases = {} khThemeAccent = imguiReady and ffi.new('float[4]', {0.72, 0.45, 0.96, 1.00}) or {[0] = 0.72, [1] = 0.45, [2] = 0.96, [3] = 1.00} khThemeCache = nil khThemeCacheR = nil khThemeCacheG = nil khThemeCacheB = nil khThemePickerOpen = false khThemeRainbowMode = false khThemeSwatches = { {name = 'Фиолетовый', color = {0.72, 0.45, 0.96}}, {name = 'Зелёный', color = {0.24, 0.78, 0.38}}, {name = 'Синий', color = {0.26, 0.52, 0.96}}, {name = 'Красный', color = {0.92, 0.30, 0.36}}, {name = 'Золото', color = {0.95, 0.70, 0.20}}, {name = 'Бирюзовый', color = {0.20, 0.78, 0.76}}, {name = 'Графит', color = {0.66, 0.66, 0.74}} } function khThemeClamp01(value) value = tonumber(value) or 0 if value < 0 then return 0 end if value > 1 then return 1 end return value end function khThemeApplyAccentRGB(r, g, b) r = khThemeClamp01(r) g = khThemeClamp01(g) b = khThemeClamp01(b) if khThemeAccent ~= nil then khThemeAccent[0] = r khThemeAccent[1] = g khThemeAccent[2] = b if khThemeAccent[3] ~= nil then khThemeAccent[3] = 1 end end khThemeCache = nil khThemeCacheR = nil khThemeCacheG = nil khThemeCacheB = nil end function khThemeSetAccentRGB(r, g, b) khThemeRainbowMode = false khThemeApplyAccentRGB(r, g, b) end function khThemeSetRainbowMode(enabled) khThemeRainbowMode = enabled and true or false khThemeCache = nil khThemeCacheR = nil khThemeCacheG = nil khThemeCacheB = nil end function khThemeRainbowRGB(nowClock) local phase = (tonumber(nowClock) or os.clock()) * 1.35 local r = 0.5 + 0.5 * math.sin(phase) local g = 0.5 + 0.5 * math.sin(phase + 2.09439510239) local b = 0.5 + 0.5 * math.sin(phase + 4.18879020479) return khThemeClamp01(r), khThemeClamp01(g), khThemeClamp01(b) end function khThemeCurrentRGB() if khThemeRainbowMode then return khThemeRainbowRGB(os.clock()) end local r = tonumber(khThemeAccent and khThemeAccent[0]) or 0.72 local g = tonumber(khThemeAccent and khThemeAccent[1]) or 0.45 local b = tonumber(khThemeAccent and khThemeAccent[2]) or 0.96 return khThemeClamp01(r), khThemeClamp01(g), khThemeClamp01(b) end function khThemeBuild(r, g, b) r = khThemeClamp01(r) g = khThemeClamp01(g) b = khThemeClamp01(b) local function clamp(v) if v < 0 then return 0 end if v > 1 then return 1 end return v end local function mix(mr, mg, mb, a, add) add = add or 0 return {clamp(r * mr + add), clamp(g * mg + add), clamp(b * mb + add), a or 1} end local function bright(mr, mg, mb, add, minV) local cr, cg, cb = clamp(r * mr + add), clamp(g * mg + add), clamp(b * mb + add) local mx = math.max(cr, cg, cb) if mx < minV then local lift = minV - mx; cr = clamp(cr + lift); cg = clamp(cg + lift); cb = clamp(cb + lift) end return {cr, cg, cb, 1.00} end return { accent = {r, g, b, 1.00}, accentText = bright(1.08, 1.08, 1.08, 0.04, 0.62), text = {0.94, 0.91, 1.00, 1.00}, muted = {0.58, 0.50, 0.70, 1.00}, mutedText = {0.82, 0.78, 0.90, 1.00}, window = {clamp(0.032 + r * 0.050), clamp(0.024 + g * 0.040), clamp(0.050 + b * 0.050), 0.98}, child = {clamp(0.052 + r * 0.060), clamp(0.035 + g * 0.050), clamp(0.075 + b * 0.060), 0.92}, border = {clamp(0.25 + r * 0.60), clamp(0.20 + g * 0.60), clamp(0.30 + b * 0.60), 0.34}, button = {clamp(0.10 + r * 0.40), clamp(0.08 + g * 0.40), clamp(0.14 + b * 0.40), 0.96}, buttonHovered = {clamp(0.14 + r * 0.50), clamp(0.10 + g * 0.50), clamp(0.18 + b * 0.50), 1.00}, buttonActive = {clamp(0.16 + r * 0.62), clamp(0.12 + g * 0.62), clamp(0.20 + b * 0.62), 1.00}, header = {clamp(0.10 + r * 0.32), clamp(0.07 + g * 0.32), clamp(0.13 + b * 0.32), 0.95}, headerHovered = {clamp(0.13 + r * 0.44), clamp(0.09 + g * 0.44), clamp(0.17 + b * 0.44), 1.00}, headerActive = {clamp(0.15 + r * 0.58), clamp(0.11 + g * 0.58), clamp(0.19 + b * 0.58), 1.00}, frame = {clamp(0.08 + r * 0.22), clamp(0.055 + g * 0.22), clamp(0.11 + b * 0.22), 0.96}, frameHovered = {clamp(0.11 + r * 0.34), clamp(0.08 + g * 0.34), clamp(0.15 + b * 0.34), 1.00}, frameActive = {clamp(0.13 + r * 0.46), clamp(0.10 + g * 0.46), clamp(0.18 + b * 0.46), 1.00}, checkMark = mix(1.10, 1.10, 1.10, 1.00, 0.04), sliderGrab = {r, g, b, 1.00}, sliderGrabActive = mix(1.14, 1.14, 1.14, 1.00, 0.04), scrollbarBg = {0.06, 0.04, 0.09, 0.88}, scrollbarGrab = {clamp(0.08 + r * 0.30), clamp(0.06 + g * 0.30), clamp(0.11 + b * 0.30), 0.92}, scrollbarGrabHovered = {clamp(0.11 + r * 0.42), clamp(0.08 + g * 0.42), clamp(0.15 + b * 0.42), 1.00}, scrollbarGrabActive = {clamp(0.14 + r * 0.56), clamp(0.10 + g * 0.56), clamp(0.18 + b * 0.56), 1.00}, tabIdle = {clamp(0.08 + r * 0.14), clamp(0.06 + g * 0.14), clamp(0.12 + b * 0.14), 0.70}, tabIdleHovered = {clamp(0.10 + r * 0.28), clamp(0.075 + g * 0.28), clamp(0.14 + b * 0.28), 0.95}, tabIdleActive = {clamp(0.13 + r * 0.40), clamp(0.095 + g * 0.40), clamp(0.17 + b * 0.40), 1.00}, bubbleBg = {clamp(0.08 + r * 0.08), clamp(0.06 + g * 0.08), clamp(0.11 + b * 0.10), 0.90}, bubbleBorder = mix(1.00, 1.00, 1.00, 0.84, 0.02) } end function khThemeCurrent() local r, g, b = khThemeCurrentRGB() if khThemeCache == nil or khThemeCacheR ~= r or khThemeCacheG ~= g or khThemeCacheB ~= b then khThemeCache = khThemeBuild(r, g, b) khThemeCacheR = r khThemeCacheG = g khThemeCacheB = b end return khThemeCache end function khThemeColor(theme, name, alpha) theme = theme or khThemeCurrent() local color = theme[name] or theme.accent or {1, 1, 1, 1} return imgui.ImVec4(color[1] or 1, color[2] or 1, color[3] or 1, alpha or color[4] or 1) end function khThemeColorU32(theme, name, alpha) return imgui.ColorConvertFloat4ToU32(khThemeColor(theme, name, alpha)) end function khThemeApplyPreset(index) local preset = khThemeSwatches[tonumber(index) or 0] if preset ~= nil and preset.color ~= nil then khThemeSetAccentRGB(preset.color[1], preset.color[2], preset.color[3]) return true end return false end local khSaveMainSettings = function() end local khApplyScriptEnabledState = function() end local khResetHudPosition = function() end local khRefreshMainTreasureBlips = function() end local khClearMainTreasureBlips = function() end local khClearMainTreasureMarker = function() end local khToggleMainPointMarker = function() end local khMainPointMarkerBusy = false local khMainPointMarkerCooldownUntil = 0 local khMainPointListCache = {} local khMainPointListCacheAt = 0 local khMaxMainPointRows = 160 -- Показываем ВСЕ точки в списке (4000+). Рендер с ручным клиппингом, чтобы не просажать FPS. local khShowAllMainPointRows = true local activeMarkerCoord = nil local activeMarkerRadius = 3.0 local activeCheckpointHandle = nil local khActiveMainPointIndex = nil local config_file = 'nakhodka.ini' local mainCfg = nil khTeamEnabled = imguiReady and new.bool(false) or {[0] = false} khTeamPort = imguiReady and new.int(27815) or {[0] = 27815} khTeam = { host = '138.124.127.115', port = 27815, token = '', manualServer = '', serverKey = 'unknown', serverLabel = 'Не определен', status = 'offline', statusText = 'Не подключено', connected = false, teamId = '', leaderToken = '', lastError = '', lastConnectAttempt = 0, lastHelloAt = 0, lastPingAt = 0, lastPosAt = 0, lastZoneAt = 0, lastMarkerRefreshAt = 0, lastServerDetectAt = 0, lastX = nil, lastY = nil, lastZ = nil, socket = nil } khTeamOutgoing = {} khTeamIncoming = {} khTeamInvites = {} khTeamMembers = {} khTeamRoster = {} khTeamRosterVisible = false khTeamRosterLastRequestAt = 0 khTeamRosterLastSeenAt = 0 khTeamRosterTtl = 20.0 khTeamMemberBlips = {} khTeamMemberZones = {} khTeamZoneIds = {} khTeamThreadStarted = false khTeamNetworkThreadActive = false khTeamLastZoneFingerprint = '' khTeamInviteInput = imguiReady and new.char[32]('') or nil khTeamServerInput = imguiReady and new.char[32]('') or nil khTeamSelectedMemberToken = nil khTeamSaveSettings = function() end khTeamQueueZoneNow = function() end local sName = '{9D6DFF}[Nakhodka]{FFFFFF} \x96 ' local rawSampAddChatMessage = sampAddChatMessage local nakhodkaNotifyTitle = 'Nakhodka' khNotifyLibReady, khNotifyLib = pcall(require, 'notify') if not khNotifyLibReady then khNotifyLib = nil end local nakhodkaNotify local function khIsScriptEnabled() return khScriptEnabled == nil or khScriptEnabled[0] end local function khCommandEnabled() if khIsScriptEnabled() then return true end nakhodkaNotify(sName .. 'Скрипт выключен. Включи его в /kh -> Основное.', -1, 'info') return false end local function setupNakhodkaNotify() if khNotifyLibReady and khNotifyLib ~= nil and type(khNotifyLib.setup) == 'function' then pcall(khNotifyLib.setup, { script_name = 'Nakhodka', width = 340, rounding = 7, margin = 20, max_on_screen = 5, appear_sec = 0.20, vanish_sec = 0.20, show_title = false, sidebar_width = 0, show_icons = true }) end end local function clearNotifyText(text) text = tostring(text or '') text = text:gsub('{%x%x%x%x%x%x}', '') text = text:gsub('^%[NAKHODKA%-RESTORE%]%s*\x96%s*', '') text = text:gsub('^%[Nakhodka%]%s*\x96%s*', '') text = text:gsub('^%[Nakhodka%]%s*\x96%s*', '') text = text:gsub('^%[NAKHODKA%-RESTORE%]%s*', '') text = text:gsub('^%[Nakhodka%]%s*', '') text = text:gsub('^%[Nakhodka%]%s*', '') text = text:gsub('^%s+', ''):gsub('%s+$', '') return text end local function getNotifyType(text) text = tostring(text or '') if text:find('{FF0000}', 1, true) or text:find('Ошибк', 1, true) or text:find('Не тот', 1, true) or text:find('Неверн', 1, true) or text:find('не удал', 1, true) or text:find('не найд', 1, true) or text:find('ничего не', 1, true) then return 'error' end if text:find('{3cb043}', 1, true) or text:find('успеш', 1, true) or text:find('Скопирован', 1, true) or text:find('найден', 1, true) or text:find('восстановлен', 1, true) or text:find('удален', 1, true) or text:find('Загружен', 1, true) or text:find('загружен', 1, true) then return 'success' end return 'info' end function nakhodkaNotify(text, color, notifyType, duration) local message = clearNotifyText(text) local kind = notifyType or getNotifyType(text) local timeout = tonumber(duration) or 3 if khNotifyLibReady and khNotifyLib ~= nil then local fn = khNotifyLib[kind] local ok = false if type(fn) == 'function' then ok = pcall(fn, nakhodkaNotifyTitle, message, timeout) elseif type(khNotifyLib.push) == 'function' then ok = pcall(khNotifyLib.push, nakhodkaNotifyTitle, message, { duration = timeout, type = kind }) end if ok then return end end rawSampAddChatMessage(text, color or -1) end setupNakhodkaNotify() function khReloadThisScript() local selfReloaded = false if type(thisScript) == 'function' then local selfScript = thisScript() if selfScript ~= nil then selfReloaded = pcall(function() selfScript:reload() end) end end if not selfReloaded and type(reloadScripts) == 'function' then reloadScripts() end end function khCheckForScriptUpdate(force) if khUpdateChecking then if force then nakhodkaNotify('Проверка обновления уже идет.', -1, 'info', 2) end return end if type(lua_thread) ~= 'table' or type(lua_thread.create) ~= 'function' then if force then nakhodkaNotify('Автообновление недоступно: нет lua_thread.', -1, 'error', 4) end return end khUpdateChecking = true lua_thread.create(function() if force then nakhodkaNotify('Проверяю обновление...', -1, 'info', 2) end local ok, requestOk, updated, status = pcall(nkBootCheckSelfUpdate) khUpdateChecking = false if ok and requestOk and updated then nakhodkaNotify('Обновление установлено. Перезагружаю скрипт...', -1, 'success', 4) wait(1200) khReloadThisScript() elseif ok and requestOk then if force then nakhodkaNotify('У тебя уже свежая версия.', -1, 'success', 3) end else local reason = ok and tostring(status or 'не удалось скачать') or tostring(requestOk or 'ошибка') if force then nakhodkaNotify('Не удалось проверить обновление: ' .. reason, -1, 'error', 5) else print('Nakhodka update check failed: ' .. reason) end end end) end local khAdditionalPoints = {} local khAdditionalBlips = {} local khDopActivePointId = nil local khDopActiveCheckpointHandle = nil local khDopExpectUntil = nil local khDopExpectStarted = nil local khLastRaceCheckpoint = nil local khDopMarkerRadius = 3.0 local khMainTreasurePoints = {} function khGetConfigDir() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local configDir = base .. '\\config' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(configDir) then if type(createDirectory) == 'function' then pcall(createDirectory, configDir) end end return configDir end function khGetMainMarksPath() return khGetConfigDir() .. '\\nakhodka_marks.dat' end function khGetLegacyMainMarksPath() return khGetConfigDir() .. '\\nakhodka_marks.json' end function khGetPhrasesPath() return khGetConfigDir() .. '\\nakhodka_phrases.json' end function khReadTextFile(path) local file = io.open(path, 'rb') if not file then return nil end local text = file:read('*a') file:close() return text end function khJsonDecodeConfig(text) text = tostring(text or ''):gsub('^\239\187\191', '') if khJsonReady and khJson ~= nil and type(khJson.decode) == 'function' then local ok, data = pcall(khJson.decode, text) if ok and type(data) == 'table' then return data end end local okDk, dkjson = pcall(require, 'dkjson') if okDk and dkjson ~= nil and type(dkjson.decode) == 'function' then local ok, data = pcall(dkjson.decode, text) if ok and type(data) == 'table' then return data end end return nil end function khJsonPoint(entry) if type(entry) ~= 'table' then return nil end local x = tonumber(entry.x or entry.X or entry[1]) local y = tonumber(entry.y or entry.Y or entry[2]) local z = tonumber(entry.z or entry.Z or entry[3]) if x == nil or y == nil or z == nil then return nil end if x ~= x or y ~= y or z ~= z then return nil end return {x, y, z} end local khMarksXorKey = 'kEyNaHodkAMeTKi' local khMarksMagic = 'NKHD2:' function khByteXor(a, b) a = tonumber(a) or 0 b = tonumber(b) or 0 if khBit ~= nil and type(khBit.bxor) == 'function' then return khBit.bxor(a, b) % 256 end local result, bitValue = 0, 1 while a > 0 or b > 0 do local abit = a % 2 local bbit = b % 2 if abit ~= bbit then result = result + bitValue end a = (a - abit) / 2 b = (b - bbit) / 2 bitValue = bitValue * 2 end return result % 256 end function khMarksMask(index, key) key = tostring(key or khMarksXorKey) local keyLen = #key if keyLen <= 0 then key = khMarksXorKey; keyLen = #key end local k1 = key:byte(((index - 1) % keyLen) + 1) or 0 local k2 = key:byte(((index * 5 + 2) % keyLen) + 1) or 0 local rolling = (137 + index * 73 + keyLen * 29 + k2 * 11) % 256 return khByteXor(khByteXor(k1, k2), rolling) end function khMarksHexByte(value) return string.format('%02X', tonumber(value) % 256) end function khMarksProtect(plain) plain = tostring(plain or '') local out = {khMarksMagic} for index = 1, #plain do local byte = plain:byte(index) or 0 local masked = khByteXor(byte, khMarksMask(index, khMarksXorKey)) masked = (masked + ((index * 31 + 91) % 256)) % 256 masked = ((masked % 16) * 16 + math.floor(masked / 16)) % 256 out[#out + 1] = khMarksHexByte(masked) end return table.concat(out) end function khMarksUnprotect(encoded) encoded = tostring(encoded or '') if encoded:sub(1, #khMarksMagic) ~= khMarksMagic then return nil end encoded = encoded:sub(#khMarksMagic + 1):gsub('%s+', '') if #encoded % 2 ~= 0 then return nil end local chars = {} local index = 1 for pos = 1, #encoded, 2 do local value = tonumber(encoded:sub(pos, pos + 1), 16) if value == nil then return nil end value = ((value % 16) * 16 + math.floor(value / 16)) % 256 value = (value - ((index * 31 + 91) % 256)) % 256 value = khByteXor(value, khMarksMask(index, khMarksXorKey)) chars[#chars + 1] = string.char(value) index = index + 1 end return table.concat(chars) end function khBuildMainMarksJson() local lines = {'{', ' "version": 2,', ' "marks": ['} for index, point in ipairs(khMainTreasurePoints or {}) do local comma = index < #khMainTreasurePoints and ',' or '' lines[#lines + 1] = string.format(' [%s, %s, %s]%s', tostring(point[1] or 0), tostring(point[2] or 0), tostring(point[3] or 0), comma) end lines[#lines + 1] = ' ]' lines[#lines + 1] = '}' return table.concat(lines, string.char(10)) .. string.char(10) end function khWriteMainMarksEncrypted() local file = io.open(khGetMainMarksPath(), 'wb') if not file then return false end file:write(khMarksProtect(khBuildMainMarksJson())) file:close() return true end function khReadMainMarksPlain() local text = khReadTextFile(khGetMainMarksPath()) local legacy = false if text == nil or text == '' then text = khReadTextFile(khGetLegacyMainMarksPath()) legacy = text ~= nil and text ~= '' end if text == nil or text == '' then return nil, false end if text:sub(1, #khMarksMagic) == khMarksMagic then return khMarksUnprotect(text), false end return text, legacy end function khLoadMainTreasurePoints() local text, legacy = khReadMainMarksPlain() if text == nil or text == '' then khWriteMainMarksEncrypted() return #khMainTreasurePoints end local data = khJsonDecodeConfig(text) local source = type(data) == 'table' and (data.marks or data.points or data) or nil local loaded = {} if type(source) == 'table' then for _, entry in ipairs(source) do local point = khJsonPoint(entry) if point ~= nil then loaded[#loaded + 1] = point end end end if #loaded > 0 then khMainTreasurePoints = loaded khMainPointListCache = {} khMainPointListCacheAt = 0 if legacy then pcall(os.remove, khGetLegacyMainMarksPath()) end khWriteMainMarksEncrypted() return #loaded end nakhodkaNotify('Не удалось загрузить nakhodka_marks.dat: нет валидных меток.', -1, 'error', 4) return #khMainTreasurePoints end local khMainTreasureBlips = {} local khMainTreasureChecked = {} local khMainTreasureLastRefresh = 0 khMainTreasureFastCheckAt = 0 local khLastDopExpireCheck = 0 local function khGetDopPointsPath() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local configDir = base .. '\\config' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(configDir) then if type(createDirectory) == 'function' then pcall(createDirectory, configDir) end end return configDir .. '\\nakhodka_dopki.txt' end local function khGetRuntimeCleanupPath() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local configDir = base .. '\\config' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(configDir) then if type(createDirectory) == 'function' then pcall(createDirectory, configDir) end end return configDir .. '\\nakhodka_runtime_cleanup.txt' end local function khWriteRuntimeCleanupArtifacts() local file = io.open(khGetRuntimeCleanupPath(), 'w') if not file then return false end file:write('created|' .. tostring(os.time()) .. '\n') local function writeNumber(kind, value) value = tonumber(value) if value ~= nil then file:write(kind .. '|' .. tostring(value) .. '\n') end end for _, blip in pairs(khMainTreasureBlips or {}) do writeNumber('blip', blip) end for _, blip in pairs(khAdditionalBlips or {}) do writeNumber('blip', blip) end for _, blip in pairs(khTeamMemberBlips or {}) do writeNumber('blip', blip) end for _, data in pairs(kh3DPickupMarkers or {}) do if type(data) == 'table' then writeNumber('user3d', data.handle) else writeNumber('user3d', data) end end writeNumber('checkpoint', activeCheckpointHandle) writeNumber('checkpoint', khDopActiveCheckpointHandle) for _, zoneId in pairs(khTeamMemberZones or {}) do writeNumber('gangzone', zoneId) end writeNumber('gangzone', 610) if activeMarkerCoord ~= nil or khDopActivePointId ~= nil then file:write('waypoint|1\n') end file:close() return true end local function khRemoveRuntimeCleanupArtifacts() local path = khGetRuntimeCleanupPath() local file = io.open(path, 'r') local actions = {} local createdAt = nil if file then for line in file:lines() do local kind, value = tostring(line or ''):match('^([^|]+)|(.+)$') if kind == 'created' then createdAt = tonumber(value) elseif kind ~= nil then table.insert(actions, {kind = kind, value = tonumber(value)}) end end file:close() pcall(os.remove, path) end if type(removeGangZone) == 'function' then pcall(removeGangZone, 610) end if createdAt ~= nil and os.time() - createdAt > 20 then return end for _, action in ipairs(actions) do if action.kind == 'blip' and action.value ~= nil and type(removeBlip) == 'function' then if type(forgetBlip) == 'function' then pcall(forgetBlip, action.value) end pcall(removeBlip, action.value) elseif action.kind == 'user3d' and action.value ~= nil and type(removeUser3dMarker) == 'function' then pcall(removeUser3dMarker, action.value) elseif action.kind == 'checkpoint' and action.value ~= nil and type(deleteCheckpoint) == 'function' then pcall(deleteCheckpoint, action.value) elseif action.kind == 'gangzone' and action.value ~= nil and type(removeGangZone) == 'function' then pcall(removeGangZone, action.value) elseif action.kind == 'waypoint' and type(removeWaypoint) == 'function' then pcall(removeWaypoint) end end end local function khDopCfgBool(value, default) if value == nil then return default end if value == true or value == 1 then return true end local text = tostring(value):lower() if text == 'true' or text == '1' then return true elseif text == 'false' or text == '0' then return false end return default end local function khEncodeAdditionalPoint(point) return string.format('%d|%.4f|%.4f|%.4f|%d|%d', tonumber(point.id) or 0, tonumber(point.x) or 0, tonumber(point.y) or 0, tonumber(point.z) or 0, tonumber(point.time or point.createdAt) or os.time(), tonumber(point.icon) or khDopDefaultIcon[0] ) end local function khInsertLoadedAdditionalPoint(id, x, y, z, savedAt, icon) x, y, z = tonumber(x), tonumber(y), tonumber(z) if not x or not y or not z then return false end local createdAt = tonumber(savedAt) or os.time() table.insert(khAdditionalPoints, { id = tonumber(id) or (#khAdditionalPoints + 1), x = x, y = y, z = z, time = createdAt, createdAt = createdAt, icon = tonumber(icon) or khDopDefaultIcon[0] }) return true end local function khLoadAdditionalPointsFromFile() local loaded = 0 local file = io.open(khGetDopPointsPath(), 'r') if not file then return loaded end for line in file:lines() do local id, x, y, z, savedAt, icon = line:match('^([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|]+)') if khInsertLoadedAdditionalPoint(id, x, y, z, savedAt, icon) then loaded = loaded + 1 end end file:close() return loaded end local function khLoadAdditionalPointsFromConfig() local loaded = 0 if mainCfg == nil or mainCfg.Dopki == nil then return loaded end local count = tonumber(mainCfg.Dopki.count) or 0 for i = 1, count do local line = mainCfg.Dopki['point' .. tostring(i)] local id, x, y, z, savedAt, icon = tostring(line or ''):match('^([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|]+)|([^|]+)') if khInsertLoadedAdditionalPoint(id, x, y, z, savedAt, icon) then loaded = loaded + 1 end end return loaded end local function khSaveAdditionalPoints() if mainCfg == nil then return false end if mainCfg.Dopki == nil then mainCfg.Dopki = {} end local oldCount = tonumber(mainCfg.Dopki.count) or 0 for i = 1, oldCount do mainCfg.Dopki['point' .. tostring(i)] = nil end mainCfg.Dopki.migrated = true mainCfg.Dopki.count = #khAdditionalPoints for i, point in ipairs(khAdditionalPoints) do mainCfg.Dopki['point' .. tostring(i)] = khEncodeAdditionalPoint(point) end return inicfg.save(mainCfg, config_file) end local function khLoadAdditionalPoints() khAdditionalPoints = {} if mainCfg == nil then return end if mainCfg.Dopki == nil then mainCfg.Dopki = {migrated = false, count = 0} end local migrated = khDopCfgBool(mainCfg.Dopki.migrated, false) local configCount = tonumber(mainCfg.Dopki.count) or 0 if migrated or configCount > 0 then khLoadAdditionalPointsFromConfig() else khLoadAdditionalPointsFromFile() khSaveAdditionalPoints() end end local function khNextDopId() local maxId = 0 for _, point in ipairs(khAdditionalPoints) do maxId = math.max(maxId, tonumber(point.id) or 0) end return maxId + 1 end local function khDistance(x1, y1, z1, x2, y2, z2) if type(getDistanceBetweenCoords3d) == 'function' then return getDistanceBetweenCoords3d(x1, y1, z1, x2, y2, z2) end local dx, dy, dz = x1 - x2, y1 - y2, z1 - z2 return math.sqrt(dx * dx + dy * dy + dz * dz) end local function khIsUniqueAdditionalPoint(x, y, z) for _, point in ipairs(khAdditionalPoints) do if khDistance(point.x, point.y, point.z, x, y, z) < 2.0 then return false end end return true end local function khBlipExists(blip) if type(blip) ~= 'number' then return false end if type(isBlipExists) == 'function' then return isBlipExists(blip) end if type(doesBlipExist) == 'function' then return doesBlipExist(blip) end if type(doesBlipExists) == 'function' then return doesBlipExists(blip) end return true end local function khForgetBlip(blip) if type(forgetBlip) == 'function' then pcall(forgetBlip, blip) end end local function khRememberBlip(blip) -- Do not remember Nakhodka blips across Ctrl+R reloads. -- Remembered blips can survive script unload and duplicate on the next start. end local function khRemoveBlip(blip) if blip and type(removeBlip) == 'function' then pcall(removeBlip, blip) end end local function khAddSpriteBlipCompat(x, y, z, icon) if type(addSpriteBlipForCoord) == 'function' then return addSpriteBlipForCoord(x, y, z, icon) end if type(addShortRangeSpriteBlipForCoord) == 'function' then return addShortRangeSpriteBlipForCoord(x, y, z, icon) end return nil end local function khClearAdditionalBlips() for _, blip in pairs(khAdditionalBlips) do if khBlipExists(blip) then khForgetBlip(blip) khRemoveBlip(blip) end end khAdditionalBlips = {} end local function khRefreshAdditionalBlips() khClearAdditionalBlips() if not khIsScriptEnabled() or khIsNoTreasureServer() or not khDopShowBlips[0] then return end for _, point in ipairs(khAdditionalPoints) do local blip = khAddSpriteBlipCompat(point.x, point.y, point.z, point.icon or khDopDefaultIcon[0]) if blip then khAdditionalBlips[point.id] = blip khRememberBlip(blip) end end end local function khAddAdditionalPoint(x, y, z) x, y, z = tonumber(x), tonumber(y), tonumber(z) if not x or not y or not z then return false end if not khIsUniqueAdditionalPoint(x, y, z) then nakhodkaNotify(sName .. '\xdd\xf2\xe0\x20\xe4\xee\xef\xea\xe0\x20\xf3\xe6\xe5\x20\xe5\xf1\xf2\xfc\x20\xe2\x20\xf1\xef\xe8\xf1\xea\xe5\x2e', -1, 'info') return false end table.insert(khAdditionalPoints, { id = khNextDopId(), x = x, y = y, z = z, time = os.time(), icon = khDopDefaultIcon[0] }) khSaveAdditionalPoints() khRefreshAdditionalBlips() nakhodkaNotify(sName .. '\xcd\xee\xe2\xe0\xff\x20\xe4\xee\xef\xea\xe0\x20\xe4\xee\xe1\xe0\xe2\xeb\xe5\xed\xe0\x20\xe2\x20\xf1\xef\xe8\xf1\xee\xea\x21', -1, 'success') return true end local function khFindDopIndexById(id) for index, point in ipairs(khAdditionalPoints) do if point.id == id then return index end end return nil end local function khClearActiveDopMarker() if khDopActiveCheckpointHandle then pcall(deleteCheckpoint, khDopActiveCheckpointHandle) khDopActiveCheckpointHandle = nil end if type(removeWaypoint) == 'function' then pcall(removeWaypoint) end khDopActivePointId = nil end local function khRemoveExpiredAdditionalPoints(showNotify) local now = os.time() local removed = 0 for index = #khAdditionalPoints, 1, -1 do local point = khAdditionalPoints[index] local createdAt = tonumber(point and (point.time or point.createdAt)) or now if now - createdAt >= 3600 then local blip = khAdditionalBlips[point.id] if blip then khForgetBlip(blip) khRemoveBlip(blip) khAdditionalBlips[point.id] = nil end if khDopActivePointId == point.id then khClearActiveDopMarker() end table.remove(khAdditionalPoints, index) removed = removed + 1 end end if removed > 0 then if khDopSelectedIndex > #khAdditionalPoints then khDopSelectedIndex = -1 end khSaveAdditionalPoints() khRefreshAdditionalBlips() if showNotify then nakhodkaNotify(sName .. string.format('Просроченные допки удалены: %d.', removed), -1, 'info', 3) end end return removed end local function khRemoveAdditionalPoint(index, silent) local point = khAdditionalPoints[index] if not point then return false end local blip = khAdditionalBlips[point.id] if blip then khForgetBlip(blip) khRemoveBlip(blip) khAdditionalBlips[point.id] = nil end if khDopActivePointId == point.id then khClearActiveDopMarker() end table.remove(khAdditionalPoints, index) if khDopSelectedIndex > #khAdditionalPoints then khDopSelectedIndex = -1 end khSaveAdditionalPoints() khRefreshAdditionalBlips() if not silent then nakhodkaNotify(sName .. '\xc4\xee\xef\xea\xe0\x20\xf3\xe4\xe0\xeb\xe5\xed\xe0\x2e', -1, 'success') end return true end local function khClearAdditionalPoints() khClearActiveDopMarker() khClearAdditionalBlips() khAdditionalPoints = {} khDopSelectedIndex = -1 khSaveAdditionalPoints() nakhodkaNotify(sName .. '\xd1\xef\xe8\xf1\xee\xea\x20\xe4\xee\xef\xee\xea\x20\xee\xf7\xe8\xf9\xe5\xed\x2e', -1, 'success') end local function khSetDopMarker(index) if khIsNoTreasureServer() then khClearActiveDopMarker() nakhodkaNotify('На Vice City кладов нет, метка не ставится.', -1, 'info', 3) return false end local point = khAdditionalPoints[index] if not point then return false end if khDopActiveCheckpointHandle then pcall(deleteCheckpoint, khDopActiveCheckpointHandle) khDopActiveCheckpointHandle = nil end if type(placeWaypoint) == 'function' then placeWaypoint(point.x, point.y, point.z) end if type(createCheckpoint) == 'function' then khDopActiveCheckpointHandle = createCheckpoint(1, point.x, point.y, point.z, 0.0, 0.0, 0.0, khDopMarkerRadius) end khDopActivePointId = point.id khDopSelectedIndex = index nakhodkaNotify(sName .. '\xcc\xe5\xf2\xea\xe0\x20\xed\xe0\x20\xe4\xee\xef\xea\xf3\x20\xef\xee\xf1\xf2\xe0\xe2\xeb\xe5\xed\xe0\x2e', -1, 'success') return true end local function khBuildSortedDopList() local px, py, pz = getCharCoordinates(PLAYER_PED) local result = {} for index, point in ipairs(khAdditionalPoints) do local dist = 0 if px and py and pz then dist = khDistance(px, py, pz, point.x, point.y, point.z) end table.insert(result, { index = index, distance = dist }) end table.sort(result, function(a, b) return a.distance < b.distance end) return result end local function khFormatDopAge(point) local createdAt = tonumber(point and (point.time or point.createdAt)) or os.time() local seconds = math.max(0, os.time() - createdAt) local minutes = math.floor(seconds / 60) if minutes < 1 then return '<1 мин.' elseif minutes < 60 then return tostring(minutes) .. ' мин.' end local hours = math.floor(minutes / 60) local restMinutes = minutes % 60 if restMinutes > 0 then return string.format('%d ч %d мин.', hours, restMinutes) end return string.format('%d ч', hours) end function khGetMyPlayerId() if type(sampGetPlayerIdByCharHandle) ~= 'function' then return nil end local ok, result, playerId = pcall(sampGetPlayerIdByCharHandle, PLAYER_PED) if not ok then return nil end if type(result) == 'number' then playerId = result elseif result ~= true then return nil end if type(playerId) == 'number' then return playerId end return nil end local function khGetMyNickname() if type(sampGetPlayerNickname) ~= 'function' then return nil end local playerId = khGetMyPlayerId() if type(playerId) ~= 'number' then return nil end local nickOk, nickname = pcall(sampGetPlayerNickname, playerId) if nickOk then return tostring(nickname or '') end return nil end khArizonaServers = { {id = 0, key = 'vicecity', label = 'Vice City', aliases = {'vicecity', 'vice city', 'vice-city', 'vc', '80.66.82.147', '80.66.82.147:7777', 'vicecity.arizona-rp.com', 'vicecity.arizona-rp.com:7777'}}, {id = 1, key = 'phoenix', label = 'Phoenix', aliases = {'phoenix', '185.169.134.3', '185.169.134.3:7777', 'phoenix.arizona-rp.com', 'phoenix.arizona-rp.com:7777'}}, {id = 2, key = 'tucson', label = 'Tucson', aliases = {'tucson', '185.169.134.4', '185.169.134.4:7777', 'tucson.arizona-rp.com', 'tucson.arizona-rp.com:7777'}}, {id = 3, key = 'scotdale', label = 'Scotdale', aliases = {'scotdale', 'scottdale', 'scottsdale', '185.169.134.43', '185.169.134.43:7777', 'scotdale.arizona-rp.com', 'scotdale.arizona-rp.com:7777'}}, {id = 4, key = 'chandler', label = 'Chandler', aliases = {'chandler', '185.169.134.44', '185.169.134.44:7777', 'chandler.arizona-rp.com', 'chandler.arizona-rp.com:7777'}}, {id = 5, key = 'brainburg', label = 'BrainBurg', aliases = {'brainburg', 'brain burg', 'brain-burg', '185.169.134.45', '185.169.134.45:7777', 'brainburg.arizona-rp.com', 'brainburg.arizona-rp.com:7777'}}, {id = 6, key = 'saintrose', label = 'Saint Rose', aliases = {'saintrose', 'saint-rose', 'saint rose', '185.169.134.5', '185.169.134.5:7777', 'saintrose.arizona-rp.com', 'saintrose.arizona-rp.com:7777'}}, {id = 7, key = 'mesa', label = 'Mesa', aliases = {'mesa', '185.169.134.59', '185.169.134.59:7777', 'mesa.arizona-rp.com', 'mesa.arizona-rp.com:7777'}}, {id = 8, key = 'redrock', label = 'Red-Rock', aliases = {'redrock', 'red-rock', 'red rock', '185.169.134.61', '185.169.134.61:7777', 'redrock.arizona-rp.com', 'redrock.arizona-rp.com:7777'}}, {id = 9, key = 'yuma', label = 'Yuma', aliases = {'yuma', '185.169.134.107', '185.169.134.107:7777', 'yuma.arizona-rp.com', 'yuma.arizona-rp.com:7777'}}, {id = 10, key = 'surprise', label = 'Surprise', aliases = {'surprise', '185.169.134.109', '185.169.134.109:7777', 'surprise.arizona-rp.com', 'surprise.arizona-rp.com:7777'}}, {id = 11, key = 'prescott', label = 'Prescott', aliases = {'prescott', '185.169.134.166', '185.169.134.166:7777', 'prescott.arizona-rp.com', 'prescott.arizona-rp.com:7777'}}, {id = 12, key = 'glendale', label = 'Glendale', aliases = {'glendale', '185.169.134.171', '185.169.134.171:7777', 'glendale.arizona-rp.com', 'glendale.arizona-rp.com:7777'}}, {id = 13, key = 'kingman', label = 'Kingman', aliases = {'kingman', '185.169.134.172', '185.169.134.172:7777', 'kingman.arizona-rp.com', 'kingman.arizona-rp.com:7777'}}, {id = 14, key = 'winslow', label = 'Winslow', aliases = {'winslow', '185.169.134.173', '185.169.134.173:7777', 'winslow.arizona-rp.com', 'winslow.arizona-rp.com:7777'}}, {id = 15, key = 'payson', label = 'Payson', aliases = {'payson', '185.169.134.174', '185.169.134.174:7777', 'payson.arizona-rp.com', 'payson.arizona-rp.com:7777'}}, {id = 16, key = 'gilbert', label = 'Gilbert', aliases = {'gilbert', '80.66.82.191', '80.66.82.191:7777', 'gilbert.arizona-rp.com', 'gilbert.arizona-rp.com:7777'}}, {id = 17, key = 'showlow', label = 'Show-Low', aliases = {'showlow', 'show-low', 'show low', '80.66.82.190', '80.66.82.190:7777', 'showlow.arizona-rp.com', 'showlow.arizona-rp.com:7777'}}, {id = 18, key = 'casagrande', label = 'Casa-Grande', aliases = {'casagrande', 'casa-grande', 'casa grande', '80.66.82.188', '80.66.82.188:7777', 'casagrande.arizona-rp.com', 'casagrande.arizona-rp.com:7777'}}, {id = 19, key = 'page', label = 'Page', aliases = {'page', '80.66.82.168', '80.66.82.168:7777', 'page.arizona-rp.com', 'page.arizona-rp.com:7777'}}, {id = 20, key = 'suncity', label = 'Sun-City', aliases = {'suncity', 'sun-city', 'sun city', '80.66.82.159', '80.66.82.159:7777', 'suncity.arizona-rp.com', 'suncity.arizona-rp.com:7777'}}, {id = 21, key = 'queencreek', label = 'Queen-Creek', aliases = {'queencreek', 'queen-creek', 'queen creek', '80.66.82.200', '80.66.82.200:7777', 'queencreek.arizona-rp.com', 'queencreek.arizona-rp.com:7777'}}, {id = 22, key = 'sedona', label = 'Sedona', aliases = {'sedona', '80.66.82.144', '80.66.82.144:7777', 'sedona.arizona-rp.com', 'sedona.arizona-rp.com:7777'}}, {id = 23, key = 'holiday', label = 'Holiday', aliases = {'holiday', '80.66.82.132', '80.66.82.132:7777', 'holiday.arizona-rp.com', 'holiday.arizona-rp.com:7777'}}, {id = 24, key = 'wednesday', label = 'Wednesday', aliases = {'wednesday', '80.66.82.128', '80.66.82.128:7777', 'wednesday.arizona-rp.com', 'wednesday.arizona-rp.com:7777'}}, {id = 25, key = 'yava', label = 'Yava', aliases = {'yava', '80.66.82.113', '80.66.82.113:7777', 'yava.arizona-rp.com', 'yava.arizona-rp.com:7777'}}, {id = 26, key = 'faraway', label = 'Faraway', aliases = {'faraway', '80.66.82.82', '80.66.82.82:7777', 'faraway.arizona-rp.com', 'faraway.arizona-rp.com:7777'}}, {id = 27, key = 'bumblebee', label = 'Bumble Bee', aliases = {'bumblebee', 'bumble bee', 'bumble-bee', '80.66.82.87', '80.66.82.87:7777', 'bumblebee.arizona-rp.com', 'bumblebee.arizona-rp.com:7777'}}, {id = 28, key = 'christmas', label = 'Christmas', aliases = {'christmas', '80.66.82.54', '80.66.82.54:7777', 'christmas.arizona-rp.com', 'christmas.arizona-rp.com:7777'}}, {id = 29, key = 'mirage', label = 'Mirage', aliases = {'mirage', '80.66.82.39', '80.66.82.39:7777', 'mirage.arizona-rp.com', 'mirage.arizona-rp.com:7777'}}, {id = 30, key = 'love', label = 'Love', aliases = {'love', '80.66.82.33', '80.66.82.33:7777', 'love.arizona-rp.com', 'love.arizona-rp.com:7777'}}, {id = 31, key = 'drake', label = 'Drake', aliases = {'drake', '80.66.82.22', '80.66.82.22:7777', 'drake.arizona-rp.com', 'drake.arizona-rp.com:7777'}}, {id = 32, key = 'space', label = 'Space', aliases = {'space', '80.66.82.199', '80.66.82.199:7777', 'space.arizona-rp.com', 'space.arizona-rp.com:7777'}} } khNoTreasureServerCacheAt = 0 khNoTreasureServerCacheValue = false function khNormalizeServerForTreasure(value) return tostring(value or ''):lower():gsub('%s+', ''):gsub('%-', ''):gsub('_', ''):gsub(':7777', '') end function khGetCurrentServerTextForTreasure() local parts = {} if type(sampGetCurrentServerAddress) == 'function' then local ok, address, port = pcall(sampGetCurrentServerAddress) if ok and address ~= nil then table.insert(parts, tostring(address)) if port ~= nil then table.insert(parts, tostring(port)) end end end if type(sampGetCurrentServerName) == 'function' then local ok, name = pcall(sampGetCurrentServerName) if ok and name ~= nil then table.insert(parts, tostring(name)) end end return table.concat(parts, ' ') end function khDetectServerKeyForTreasure() local text = khGetCurrentServerTextForTreasure() local normalizedText = khNormalizeServerForTreasure(text) if normalizedText == '' then return tostring(khTeam and khTeam.serverKey or '') end for _, server in ipairs(khArizonaServers or {}) do if normalizedText:find(khNormalizeServerForTreasure(server.key), 1, true) then return server.key end for _, alias in ipairs(server.aliases or {}) do local normalizedAlias = khNormalizeServerForTreasure(alias) if normalizedAlias ~= '' and normalizedText:find(normalizedAlias, 1, true) then return server.key end end end return tostring(khTeam and khTeam.serverKey or '') end function khIsNoTreasureServer() local nowClock = os.clock() if nowClock - (tonumber(khNoTreasureServerCacheAt) or 0) < 1.0 then return khNoTreasureServerCacheValue end khNoTreasureServerCacheAt = nowClock khNoTreasureServerCacheValue = khDetectServerKeyForTreasure() == 'vicecity' return khNoTreasureServerCacheValue end function khTeamRequestRoster(force) end khTeamQueueZoneNow = function() end function khStartTeamClient() end function khTeamUpdate(now) end function khClearTeamMapArtifacts() end function khTeamCmdOpen() end function khTeamCmdInvite(arg) end function khTeamCmdAccept(arg) end function khTeamCmdDeny(arg) end function khTeamCmdLeave() end function khTeamCmdKick(arg) end function khTeamCmdServer(arg) end function khTeamCmdReconnect() end function khTeamCmdSos() end local function khNormalizeName(name) return tostring(name or ''):gsub('^%s+', ''):gsub('%s+$', ''):lower() end local function khIsOwnDopMessage(message) message = tostring(message or '') if not message:find('\x5b\xca\xeb\xe0\xe4\xfb\x5d', 1, true) or not message:find('\xca\xeb\xe0\xe4\xee\xe8\xf1\xea\xe0\xf2\xe5\xeb\xfc', 1, true) or not message:find('\xef\xee\xef\xfb\xf2\xe0\xeb\x20\xf3\xe4\xe0\xf7\xf3\x20\xe8\x20\xef\xee\xeb\xf3\xf7\xe8\xeb\x20\xf1\xe5\xea\xf0\xe5\xf2\xed\xfb\xe9\x20\xe4\xee\xef\xee\xeb\xed\xe8\xf2\xe5\xeb\xfc\xed\xfb\xe9\x20\xea\xeb\xe0\xe4', 1, true) then return false end local nickname = message:match('\xca\xeb\xe0\xe4\xee\xe8\xf1\xea\xe0\xf2\xe5\xeb\xfc%s+([^%[]+)%[%d+%]') local myNickname = khGetMyNickname() if not nickname or not myNickname then return false end return khNormalizeName(nickname) == khNormalizeName(myNickname) end function khIsOwnKladDigMessage(message) message = tostring(message or '') if not message:find('[Клады]', 1, true) or not message:find('Кладоискатель', 1, true) or not message:find('выкопал клад и забрал свой куш', 1, true) then return false end local nickname = message:match('Кладоискатель%s+([^%[]+)%[%d+%]') local myNickname = khGetMyNickname() if not nickname or not myNickname then return false end return khNormalizeName(nickname) == khNormalizeName(myNickname) end local function khRememberRaceCheckpoint(position) if type(position) ~= 'table' then return end local x = tonumber(position.x or position.X or position[1]) local y = tonumber(position.y or position.Y or position[2]) local z = tonumber(position.z or position.Z or position[3]) if not x or not y or not z then return end khLastRaceCheckpoint = { x = x, y = y, z = z, t = os.clock() } if khDopExpectUntil and os.clock() <= khDopExpectUntil then khAddAdditionalPoint(x, y, z) khDopExpectUntil = nil khDopExpectStarted = nil end end local function khStartExpectingDopCheckpoint() khDopExpectStarted = os.clock() khDopExpectUntil = khDopExpectStarted + 4.0 end local function khCheckDopExpectation() if not khDopExpectUntil then return end if os.clock() <= khDopExpectUntil then return end if khLastRaceCheckpoint and khDopExpectStarted and khLastRaceCheckpoint.t >= khDopExpectStarted - 1.0 then khAddAdditionalPoint(khLastRaceCheckpoint.x, khLastRaceCheckpoint.y, khLastRaceCheckpoint.z) end khDopExpectUntil = nil khDopExpectStarted = nil end local function khTryRemoveActiveDopByDig(silent) local index = khDopActivePointId and khFindDopIndexById(khDopActivePointId) or nil local point = index and khAdditionalPoints[index] or nil if khDopActivePointId ~= nil and not point then khClearActiveDopMarker() end local x, y, z = getCharCoordinates(PLAYER_PED) local removeRadius = 28.0 if x and y and z then local bestIndex, bestDistance = nil, 999999.0 for dopIndex, dopPoint in ipairs(khAdditionalPoints) do local dx = x - (tonumber(dopPoint.x) or 0) local dy = y - (tonumber(dopPoint.y) or 0) local dist = math.sqrt(dx * dx + dy * dy) if dist <= removeRadius and dist < bestDistance then bestIndex, bestDistance = dopIndex, dist end end local activeDistance = nil if point then local dx = x - (tonumber(point.x) or 0) local dy = y - (tonumber(point.y) or 0) activeDistance = math.sqrt(dx * dx + dy * dy) end if bestIndex ~= nil and (not point or activeDistance == nil or activeDistance > removeRadius or bestDistance < activeDistance) then index = bestIndex point = khAdditionalPoints[index] end end if x and y and z and point then local dx = x - (tonumber(point.x) or 0) local dy = y - (tonumber(point.y) or 0) if math.sqrt(dx * dx + dy * dy) > removeRadius then return false end khRemoveAdditionalPoint(index, true) if not silent then nakhodkaNotify(sName .. '\xc4\xee\xef\xea\xe0\x20\xe2\xfb\xea\xee\xef\xe0\xed\xe0\x20\xe8\x20\xf3\xe4\xe0\xeb\xe5\xed\xe0\x20\xe8\xe7\x20\xf1\xef\xe8\xf1\xea\xe0\x2e', -1, 'success') khTyanCelebrate('dop') end return true end return false end local function khHandleDopServerMessage(message, silent) message = tostring(message or '') if message:find('\xc2\xfb\x20\xf3\xf1\xef\xe5\xf8\xed\xee\x20\xe4\xee\xf1\xf2\xe0\xeb\xe8\x20\xe8\xe7\x20\xea\xeb\xe0\xe4\xe0', 1, true) then khTryRemoveActiveDopByDig(silent) end if khIsOwnKladDigMessage(message) then khTryRemoveActiveDopByDig(silent) end if khIsOwnDopMessage(message) then khTryRemoveActiveDopByDig(silent) if not silent and khIsScriptEnabled() then khStartExpectingDopCheckpoint() end end end khDropLogs = {} khDropPrices = {} khPriceInputs = {} khAveragePricesByName = nil khAveragePricesLoaded = false khAveragePriceItemCache = {} khDropProfitCacheValue = 0 khDropProfitCacheAt = 0 khDropProfitCacheLogCount = -1 function khInvalidateDropProfitCache() khDropProfitCacheAt = 0 khDropProfitCacheLogCount = -1 end function khInvalidateAveragePriceCache() khAveragePriceItemCache = {} khInvalidateDropProfitCache() end khCurrentDropLogId = nil khLastDropAt = 0 khPendingDropSession = nil khDropInvCounts = {} khDropTempInvCounts = {} khDropExpectedItems = {} khDropPendingItemName = nil khDropPendingItemCount = 1 khDropPendingAddedItems = {} khDropRecentlyQueuedByAdded = {} khDropInvReady = false khDropInvSyncState = 0 khDropInvSyncMode = 'idle' khDropStatsSilentUntil = 0 khDropStatsHadDialog = false khDropSyncQueuedAfterCurrent = false khDropLastPreDigSyncAt = 0 khDropItemNameMap = nil local KH_DROP_SYNC_IDLE = 0 local KH_DROP_SYNC_WAIT_STATS = 1 local KH_DROP_SYNC_SCANNING = 2 local function khGetDropStatsPath() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local configDir = base .. '\\config' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(configDir) then if type(createDirectory) == 'function' then pcall(createDirectory, configDir) end end return configDir .. '\\nakhodka_drop_stats.txt' end function khGetDropPricesPath() return khGetDropStatsPath():gsub('nakhodka_drop_stats%.txt$', 'nakhodka_prices.txt') end khAvgPriceUrl = 'https://cdn.jsdelivr.net/gh/FREYM1337/forumnick@main/avg_price/info_users_sell_vc.json' khAvgPriceUrls = { khAvgPriceUrl, 'https://raw.githubusercontent.com/FREYM1337/forumnick/main/avg_price/info_users_sell_vc.json', 'https://raw.githubusercontent.com/FREYM1337/forumnick/refs/heads/main/avg_price/info_users_sell_vc.json' } khAvgPriceLoading = false local function khEscapeDropText(text) text = tostring(text or '') text = text:gsub(string.char(13), ''):gsub(string.char(10), ' ') return text end local function khUnescapeDropText(text) text = tostring(text or '') text = text:gsub('%%7E', '~'):gsub('%%3B', ';'):gsub('%%7C', '|'):gsub('%%25', '%%') return text end local function khLowerCp1251(text) text = tostring(text or '') local result = {} for i = 1, #text do local b = text:byte(i) if b >= 192 and b <= 223 then result[#result + 1] = string.char(b + 32) elseif b == 168 then result[#result + 1] = string.char(184) else result[#result + 1] = string.lower(string.char(b)) end end return table.concat(result) end function khDropDecodeUtf8(text) text = tostring(text or '') if text == '' or u8 == nil then return text end local ok, decoded = pcall(function() return u8:decode(text) end) if ok and decoded ~= nil and decoded ~= '' then return decoded end return text end function khDropWorkDir() if type(getWorkingDirectory) == 'function' then local ok, dir = pcall(getWorkingDirectory) if ok and dir ~= nil and tostring(dir) ~= '' then return tostring(dir) end end return 'moonloader' end function khDropReadFile(path) local file = io.open(tostring(path or ''), 'rb') if not file then return nil end local text = file:read('*a') file:close() return text end function khDropLoadItemNameMap() if khDropItemNameMap ~= nil then return khDropItemNameMap end khDropItemNameMap = {} local base = khDropWorkDir() .. '\\ArzMarket\\' local files = { 'items_data28.json', 'items_data27.json', 'items_data23.json', 'items_data22.json', 'items_data21.json', 'items_data19.json', 'items_data18.json', 'items_data17.json', 'items_data16.json', 'items_data15.json', 'items_data12.json', 'items_data5.json', 'items_data0.json', 'items_data.json', 'items.json' } for _, fileName in ipairs(files) do local text = khDropReadFile(base .. fileName) if text ~= nil and text ~= '' then for id, name in text:gmatch('"(%d+)"%s*:%s*"(.-)"') do if khDropItemNameMap[id] == nil then name = khDropDecodeUtf8(name:gsub('\\/', '/'):gsub('\\"', '"')) name = name:gsub('%s*%(ID:%s*%d+%)%s*$', '') if name ~= '' then khDropItemNameMap[id] = name end end end end end return khDropItemNameMap end function khResolveDropItemToken(text) text = tostring(text or '') local id = text:match('^%s*:item(%d+):%s*$') or text:match('^%s*item(%d+)%s*$') or text:match('^%s*%[item(%d+)%]%s*$') if id == nil then return text end local map = khDropLoadItemNameMap() local name = map and map[id] or nil if name ~= nil and name ~= '' then return name end return 'Предмет #' .. tostring(id) end local function khCleanDropItemName(text) text = clearNotifyText(text) text = text:gsub('^%s+', ''):gsub('%s+$', '') text = text:gsub("^'(.-)'$", '%1'):gsub('^"(.-)"$', '%1') text = text:gsub('%.+$', '') text = text:gsub('^%s+', ''):gsub('%s+$', '') return khResolveDropItemToken(text) end function khIsTreasureBoxDropItem(itemName) return khLowerCp1251(tostring(itemName or '')) == khLowerCp1251('Ларец кладоискателя') end local function khDropLogItemCount(log) if type(log) ~= 'table' or type(log.items) ~= 'table' then return 0 end local total = 0 for _, item in ipairs(log.items) do if type(item) == 'table' and tostring(item.name or '') ~= '' then total = total + 1 end end return total end function khFormatNumber(value) local n = math.floor((tonumber(value) or 0) + 0.5) local sign = '' if n < 0 then sign, n = '-', -n end local text = tostring(n) while true do local nextText, changed = text:gsub('^(%d+)(%d%d%d)', '%1.%2') text = nextText if changed == 0 then break end end return sign .. text end local function khNextDropLogId() local maxId = 0 for _, log in ipairs(khDropLogs) do maxId = math.max(maxId, tonumber(log.id) or 0) end return maxId + 1 end local function khSaveDropStats() local file = io.open(khGetDropStatsPath(), 'w') if not file then return false end for _, log in ipairs(khDropLogs) do local chunks = {} for _, item in ipairs(log.items or {}) do local itemCount = tonumber(item.count) or 1 if khIsTreasureBoxDropItem(item.name) then itemCount = 1 end table.insert(chunks, khEscapeDropText(item.name) .. '~' .. tostring(itemCount)) end file:write(string.format('%d|%d|%s|%s|%s', tonumber(log.id) or 0, tonumber(log.timestamp) or os.time(), tostring(log.date or ''), tostring(log.time or ''), table.concat(chunks, ';')) .. string.char(10)) end file:close() khInvalidateDropProfitCache() return true end local function khLoadDropStats() khDropLogs = {} local file = io.open(khGetDropStatsPath(), 'r') if not file then khSaveDropStats(); return end for line in file:lines() do local id, ts, dateText, timeText, itemsText = line:match('^([^|]*)|([^|]*)|([^|]*)|([^|]*)|(.*)$') if id and ts and dateText and timeText then local log = {id = tonumber(id) or (#khDropLogs + 1), timestamp = tonumber(ts) or os.time(), date = tostring(dateText or ''), time = tostring(timeText or ''), items = {}} for chunk in tostring(itemsText or ''):gmatch('[^;]+') do local name, count = chunk:match('^(.-)~([^~]*)$') if name and name ~= '' then local cleanName = khCleanDropItemName(khUnescapeDropText(name)) local itemCount = math.max(1, math.floor((tonumber(count) or 1) + 0.5)) local isTreasureBox = khIsTreasureBoxDropItem(cleanName) if isTreasureBox then itemCount = 1 end local merged = false for _, item in ipairs(log.items) do if tostring(item.name or '') == cleanName then if isTreasureBox then item.count = 1 else item.count = (tonumber(item.count) or 0) + itemCount end merged = true break end end if cleanName ~= '' and not merged then table.insert(log.items, {name = cleanName, count = itemCount}) end end end table.insert(khDropLogs, log) end end file:close() khInvalidateDropProfitCache() end local function khGetDropLogById(id) for _, log in ipairs(khDropLogs) do if tostring(log.id) == tostring(id) then return log end end return nil end local function khGetDropDays() local seen, days = {}, {} for _, log in ipairs(khDropLogs) do local dateText = tostring(log.date or '') if dateText ~= '' and not seen[dateText] then seen[dateText] = true; table.insert(days, dateText) end end table.sort(days, function(a, b) return tostring(a) > tostring(b) end) return days end local function khGetDropLogsForDay(dateText) local result = {} for _, log in ipairs(khDropLogs) do if tostring(log.date or '') == tostring(dateText or '') then table.insert(result, log) end end table.sort(result, function(a, b) return (tonumber(a.timestamp) or 0) > (tonumber(b.timestamp) or 0) end) return result end function khGetDayDropProfit(dateText) local total = 0 for _, log in ipairs(khDropLogs) do if tostring(log.date or '') == tostring(dateText or '') then total = total + khGetDropProfit(log) end end return math.floor(total + 0.5) end local function khGetTodayDropCount() local today, total = os.date('%Y-%m-%d'), 0 for _, log in ipairs(khDropLogs) do if log.date == today then total = total + 1 end end return total end local function khGetTotalDropCount() return #khDropLogs end function khSaveDropPrices() local file = io.open(khGetDropPricesPath(), 'w') if not file then return false end local names = {} for name, _ in pairs(khDropPrices) do table.insert(names, tostring(name)) end table.sort(names) for _, name in ipairs(names) do file:write(khEscapeDropText(name) .. '|' .. tostring(math.max(0, math.floor((tonumber(khDropPrices[name]) or 0) + 0.5))) .. string.char(10)) end file:close() khInvalidateDropProfitCache() return true end function khLoadDropPrices() khDropPrices, khPriceInputs = {}, {} local file = io.open(khGetDropPricesPath(), 'r') if not file then khSaveDropPrices(); return end for line in file:lines() do local name, price = line:match('^(.-)|([^|]*)$') if name and name ~= '' then khDropPrices[khCleanDropItemName(khUnescapeDropText(name))] = math.max(0, math.floor((tonumber(price) or 0) + 0.5)) end end file:close() khInvalidateAveragePriceCache() end function khGetDropItemPrice(name) local cleanName = khCleanDropItemName(name) local stored = khDropPrices[cleanName] if stored ~= nil then return math.max(0, math.floor((tonumber(stored) or 0) + 0.5)) end local avgPrice = khFindAveragePriceForItem(cleanName) if avgPrice ~= nil and avgPrice > 0 then if khPriceInputs[cleanName] ~= nil then khPriceInputs[cleanName][0] = avgPrice end return avgPrice end return 0 end function khAveragePricePath() return khGetDropStatsPath():gsub('nakhodka_drop_stats%.txt$', 'nakhodka_avg_prices.json') end function khNormalizePriceName(text) text = khCleanDropItemName(text) text = text:gsub('^%s*%[%d+%]%s*', '') text = text:gsub('%s*%(ID:%s*%d+%)', '') text = text:gsub('%s*%(id:%s*%d+%)', '') text = khLowerCp1251(text) text = text:gsub('ё', 'е') text = text:gsub('[%s%p%c]+', '') return text end function khAverageEntryPrice(entry) if type(entry) ~= 'table' then return nil end if type(entry.list) == 'table' then for _, row in ipairs(entry.list) do if type(row) == 'table' then local price = tonumber(row[6]) or tonumber(row[5]) or tonumber(row[3]) if price ~= nil and price > 0 then return math.floor(price + 0.5) end end end end local price = tonumber(entry.avg) or tonumber(entry.average) or tonumber(entry.price) if price ~= nil and price > 0 then return math.floor(price + 0.5) end return nil end function khReadAveragePriceFile(path) local file = io.open(path, 'rb') if not file then return nil end local text = file:read('*a') file:close() return text end function khWriteAveragePriceFile(text) local file = io.open(khAveragePricePath(), 'wb') if not file then return false end file:write(tostring(text or '')) file:close() return true end function khDecodeAverageJson(text) local decoders = {} if khJsonReady and khJson ~= nil and type(khJson.decode) == 'function' then table.insert(decoders, khJson) end local okDk, dkjson = pcall(require, 'dkjson') if okDk and dkjson ~= nil and type(dkjson.decode) == 'function' and dkjson ~= khJson then table.insert(decoders, dkjson) end local lastError = 'JSON-библиотека недоступна' for _, decoder in ipairs(decoders) do local ok, data = pcall(decoder.decode, tostring(text or '')) if ok and type(data) == 'table' then return true, data end lastError = tostring(data) end return false, lastError end function khBuildAveragePriceMap(data) local averageByName = {} local count = 0 if type(data) ~= 'table' then return averageByName, count end for jsonName, entry in pairs(data) do local key = khNormalizePriceName(jsonName) local price = khAverageEntryPrice(entry) if key ~= '' and price ~= nil and price > 0 then averageByName[key] = price count = count + 1 end end return averageByName, count end function khEnsureAveragePricesLoaded() if khAveragePricesLoaded then return khAveragePricesByName or {} end khAveragePricesLoaded = true khAveragePricesByName = {} local text = khReadAveragePriceFile(khAveragePricePath()) if text == nil or tostring(text) == '' then return khAveragePricesByName end local ok, data = khDecodeAverageJson(text) if ok and type(data) == 'table' then khAveragePricesByName = khBuildAveragePriceMap(data) end khAveragePriceItemCache = {} return khAveragePricesByName or {} end function khFindAveragePriceInMap(name, averageByName) local key = khNormalizePriceName(name) if key == '' or type(averageByName) ~= 'table' then return nil end local price = averageByName[key] if price == nil and #key >= 8 then for avgKey, avgPrice in pairs(averageByName) do if avgKey == key or avgKey:find(key, 1, true) or key:find(avgKey, 1, true) then price = avgPrice break end end end if price ~= nil and price > 0 then return math.floor(price + 0.5) end return nil end function khFindAveragePriceForItem(name) local cleanName = khCleanDropItemName(name) if cleanName == '' then return nil end local cached = khAveragePriceItemCache[cleanName] if cached ~= nil then if cached > 0 then return cached end return nil end local price = khFindAveragePriceInMap(cleanName, khEnsureAveragePricesLoaded()) khAveragePriceItemCache[cleanName] = price or -1 return price end function khAsyncHttpRequest(method, url, args, resolve, reject) resolve = resolve or function() end reject = reject or function() end args = args or {} local threadTimeout = math.max(18, tonumber(args.kh_thread_timeout or args.thread_timeout or args.timeout) or 18) args.kh_thread_timeout = nil args.thread_timeout = nil if not khEffilReady or khEffil == nil or type(khEffil.thread) ~= 'function' then reject('effil недоступен') return end local workerSource = [[ return function(method, url, args) local result, response = pcall(function() local requests = require 'requests' return requests.request(tostring(method or 'GET'), tostring(url or ''), type(args) == 'table' and args or {}) end) if result then if type(response) == 'table' then response.json, response.xml = nil, nil return true, response elseif response == nil then return false, 'Empty response' end return false, 'Invalid response type: ' .. type(response) end return false, tostring(response) end ]] local okWorker, worker = pcall(function() return loadstring(workerSource)() end) if not okWorker or type(worker) ~= 'function' then reject('не удалось создать поток запроса') return end method = tostring(method or 'GET') url = tostring(url or '') local okThread, thread = pcall(function() return khEffil.thread(worker)(method, url, args) end) if not okThread or thread == nil then reject('не удалось запустить поток запроса') return end lua_thread.create(function() local startedAt = os.clock() while true do local status, statusError = thread:status() if status == 'completed' then local okGet, requestOk, response = pcall(thread.get, thread) if okGet then if requestOk then resolve(response) else reject(response) end else reject('ошибка получения ответа: ' .. tostring(requestOk)) end return elseif status == 'canceled' then reject('запрос отменен') return elseif status == 'failed' or statusError ~= nil then reject(statusError or status) return elseif os.clock() - startedAt > threadTimeout then pcall(function() thread:cancel(0) end) reject('timeout') return end wait(0) end end) end function khApplyAveragePriceText(text) local ok, data = khDecodeAverageJson(text) if not ok or type(data) ~= 'table' then nakhodkaNotify('Не удалось разобрать файл средних цен.', -1, 'error', 4) return false end local averageByName, averageCount = khBuildAveragePriceMap(data) khAveragePricesByName = averageByName khAveragePricesLoaded = true khAveragePriceItemCache = {} local items = khGetUniqueDropItems() local loaded = 0 for _, name in ipairs(items) do local price = khFindAveragePriceInMap(name, averageByName) if price ~= nil and price > 0 then khDropPrices[name] = price if khPriceInputs[name] ~= nil then khPriceInputs[name][0] = price end loaded = loaded + 1 end end khSaveDropPrices() if loaded > 0 then nakhodkaNotify(string.format('Средние цены загружены: %d из %d. База сохранена: %d.', loaded, #items, averageCount or 0), -1, 'success', 4) return true end nakhodkaNotify(string.format('Средние цены сохранены: %d. Новые предметы получат цену автоматически.', averageCount or 0), -1, 'info', 4) return false end function khStartLoadAveragePrices() if khAvgPriceLoading then nakhodkaNotify('Средние цены уже загружаются.', -1, 'info', 2) return end khAvgPriceLoading = true nakhodkaNotify('Загружаю средние цены...', -1, 'info', 2) local urls = {} if type(khAvgPriceUrls) == 'table' then for _, url in ipairs(khAvgPriceUrls) do if tostring(url or '') ~= '' then table.insert(urls, tostring(url)) end end end if #urls == 0 and tostring(khAvgPriceUrl or '') ~= '' then table.insert(urls, tostring(khAvgPriceUrl)) end local index = 1 local lastError = 'нет доступных ссылок' local function useCachedOrFail() khAvgPriceLoading = false local cached = khReadAveragePriceFile(khAveragePricePath()) if cached ~= nil and tostring(cached) ~= '' then nakhodkaNotify('Сеть не ответила, использую сохраненные средние цены.', -1, 'info', 4) khApplyAveragePriceText(cached) return end nakhodkaNotify('Ошибка загрузки средних цен: ' .. tostring(lastError), -1, 'error', 4) end local function tryNextUrl() local url = urls[index] index = index + 1 if url == nil then useCachedOrFail() return end khAsyncHttpRequest('GET', url, { timeout = 25, kh_thread_timeout = 45, headers = {['User-Agent'] = 'Nakhodka'} }, function(response) local statusCode = tonumber(response and response.status_code) if statusCode ~= nil and (statusCode < 200 or statusCode >= 300) then lastError = 'HTTP ' .. tostring(statusCode) tryNextUrl() return end local text = tostring((response and response.text) or '') if text == '' then lastError = 'пустой ответ' tryNextUrl() return end khAvgPriceLoading = false khWriteAveragePriceFile(text) khApplyAveragePriceText(text) end, function(err) lastError = tostring(err) tryNextUrl() end) end tryNextUrl() end function khGetDropItemTotalCount(name) local total = 0 name = tostring(name or '') for _, log in ipairs(khDropLogs) do for _, item in ipairs(log.items or {}) do if tostring(item.name or '') == name then total = total + (tonumber(item.count) or 1) end end end return total end function khGetUniqueDropItems() local seen, items = {}, {} for _, log in ipairs(khDropLogs) do for _, item in ipairs(log.items or {}) do local name = tostring(item.name or '') if name ~= '' and not seen[name] then seen[name] = true; table.insert(items, name) end end end table.sort(items) return items end function khPruneDropPricesToKnownItems(saveNow) local known = {} for _, name in ipairs(khGetUniqueDropItems()) do known[tostring(name or '')] = true end local changed = false for name, _ in pairs(khDropPrices) do if not known[tostring(name or '')] then khDropPrices[name] = nil khPriceInputs[name] = nil changed = true end end if changed then khInvalidateDropProfitCache() if saveNow then khSaveDropPrices() end end end function khGetDropProfit(log) local useGlobalCache = not (type(log) == 'table' and type(log.items) == 'table') if useGlobalCache then local nowClock = os.clock() if khDropProfitCacheAt > 0 and khDropProfitCacheLogCount == #khDropLogs and nowClock - khDropProfitCacheAt < 0.50 then return khDropProfitCacheValue end end local total = 0 local logs = (type(log) == 'table' and type(log.items) == 'table') and {log} or khDropLogs for _, dropLog in ipairs(logs) do for _, item in ipairs(dropLog.items or {}) do total = total + (tonumber(item.count) or 1) * khGetDropItemPrice(item.name) end end total = math.floor(total + 0.5) if useGlobalCache then khDropProfitCacheValue = total khDropProfitCacheAt = os.clock() khDropProfitCacheLogCount = #khDropLogs end return total end local function khParseItemNameAndCount(text) text = khCleanDropItemName(text) local count = 1 local name, parsedCount = text:match('^(.-)%s*%((%d+)%s*шт%.?%)') if name and tonumber(parsedCount) then text, count = khCleanDropItemName(name), tonumber(parsedCount) else name, parsedCount = text:match('^(.-)%s+[xX](%d+)$') if name and tonumber(parsedCount) then text, count = khCleanDropItemName(name), tonumber(parsedCount) else name, parsedCount = text:match('^(.-)%s+(%d+)%s*шт%.?$') if name and tonumber(parsedCount) then text, count = khCleanDropItemName(name), tonumber(parsedCount) end end end local amountPrefix, amountName = text:match('^(%d+)%s+(.+)$') if amountPrefix and amountName then local lowerAmountName = khLowerCp1251(amountName) if lowerAmountName:find('евро', 1, true) or lowerAmountName:find('руб', 1, true) or lowerAmountName:find('vc', 1, true) or lowerAmountName:find('вирт', 1, true) or lowerAmountName:find('az', 1, true) or lowerAmountName:find('coin', 1, true) or lowerAmountName:find('аз', 1, true) then text = khCleanDropItemName(amountName) count = tonumber(amountPrefix) or count end end if text == '' then return nil, nil end return text, math.max(1, math.floor((tonumber(count) or 1) + 0.5)) end local function khParseAddedItemMessage(message) message = clearNotifyText(message) local rest = message:match('Вам%s+был%s+добавлен%s+предмет%s+(.+)') if not rest then return nil, nil end rest = rest:gsub('%s*Откройте.+$', '') return khParseItemNameAndCount(rest) end function khIsPendingAddedBlocked(name) local lower = khLowerCp1251(name) return lower:find('payday', 1, true) or lower:find('az coins', 1, true) or lower:find('az-coins', 1, true) or lower:find('az coin', 1, true) or lower:find('az-coin', 1, true) end function khRememberPendingAddedItem(itemName, count) itemName = khCleanDropItemName(itemName) count = math.max(1, math.floor((tonumber(count) or 1) + 0.5)) if itemName == '' then return end local nowClock = os.clock() table.insert(khDropPendingAddedItems, {name = itemName, count = count, clock = nowClock}) khDropPendingItemName = itemName khDropPendingItemCount = count for index = #khDropPendingAddedItems, 1, -1 do if nowClock - (tonumber(khDropPendingAddedItems[index].clock) or 0) > 12.0 or index < #khDropPendingAddedItems - 24 then table.remove(khDropPendingAddedItems, index) end end end function khTakeRecentPendingAddedItems(maxAge, allowBlocked) local nowClock = os.clock() local result = {} for index = #khDropPendingAddedItems, 1, -1 do local item = khDropPendingAddedItems[index] local age = nowClock - (tonumber(item.clock) or 0) if age <= (tonumber(maxAge) or 6.0) then if allowBlocked or not khIsPendingAddedBlocked(item.name) then table.insert(result, 1, {name = item.name, count = tonumber(item.count) or 1}) end table.remove(khDropPendingAddedItems, index) elseif age > 12.0 then table.remove(khDropPendingAddedItems, index) end end return result end local function khParseDropMessage(message) message = clearNotifyText(message) local itemText = message:match('Вы%s+успешно%s+достали%s+из%s+клада%s+(.+)') if not itemText then itemText = message:match('Вы%s+успешно%s+вскопали%s+клад[:%.%s]+(.+)') end if not itemText then itemText = message:match('Вы%s+успешно%s+выкопали%s+клад[:%.%s]+(.+)') end if not itemText then return nil, nil end return khParseItemNameAndCount(itemText) end local function khEnsurePendingDropSession() if khPendingDropSession == nil then local now = os.time() khPendingDropSession = {timestamp = now, date = os.date('%Y-%m-%d', now), time = os.date('%H:%M:%S', now), items = {}, lastDropClock = os.clock()} end return khPendingDropSession end local function khAddPendingDropItem(itemName, count) itemName = khCleanDropItemName(itemName) count = math.max(1, math.floor((tonumber(count) or 1) + 0.5)) if itemName == '' then return end local isTreasureBox = khIsTreasureBoxDropItem(itemName) if isTreasureBox then count = 1 end local session = khEnsurePendingDropSession() session.lastDropClock = os.clock() khLastDropAt = session.lastDropClock for _, item in ipairs(session.items) do if tostring(item.name) == tostring(itemName) then if isTreasureBox then item.count = 1 else item.count = (tonumber(item.count) or 1) + count end return end end table.insert(session.items, {name = itemName, count = count}) end local function khQueueExpectedDropItem(itemName, fallbackCount) itemName = khCleanDropItemName(itemName) local safeName = khLowerCp1251(itemName) if safeName == '' then return end local existing = khDropExpectedItems[safeName] if existing then existing.fallback = (tonumber(existing.fallback) or 0) + (tonumber(fallbackCount) or 1) else khDropExpectedItems[safeName] = {name = itemName, fallback = tonumber(fallbackCount) or 1} end end function khQueueTreasureBoxExpected(session) session = session or khEnsurePendingDropSession() if session.boxQueued then return end khAddPendingDropItem('Ларец кладоискателя', 1) session.boxQueued = true end local function khIsDropStatsDialog(title, text) local joined = clearNotifyText(tostring(title or '') .. ' ' .. tostring(text or '')) local lower = khLowerCp1251(joined) return lower:find('основная статистика', 1, true) or lower:find('текущее состояние счета', 1, true) or lower:find('авторизация на сервере', 1, true) or lower:find('предметы', 1, true) and lower:find('закрыть', 1, true) or lower:find('инвентарь', 1, true) end local function khCloseDropStatsDialog(id, delay) local waitMs = delay or 0 local function close() if type(sampSendDialogResponse) == 'function' then pcall(sampSendDialogResponse, id, 0, 0, '') end if type(sampCloseCurrentDialogWithButton) == 'function' then pcall(sampCloseCurrentDialogWithButton, 0) end end if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() wait(waitMs); close() end) else close() end end local function khDialogResponseLater(id, button, listItem, input, delay) local function send() if type(sampSendDialogResponse) == 'function' then pcall(sampSendDialogResponse, id, button, listItem, input or '') end end if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() wait(delay or 0); send() end) else send() end end function khStartDropInventorySync(mode) if khDropInvSyncState ~= KH_DROP_SYNC_IDLE then return false end if type(sampSendChat) ~= 'function' then return false end khDropInvSyncState = KH_DROP_SYNC_WAIT_STATS khDropInvSyncMode = mode or 'manual' khDropStatsSilentUntil = os.clock() + 15.0 khDropStatsHadDialog = false sampSendChat('/stats') return true end function khStartQueuedDropInventorySync(delay) if not khDropSyncQueuedAfterCurrent or next(khDropExpectedItems or {}) == nil then khDropSyncQueuedAfterCurrent = false return false end local function run() if khDropSyncQueuedAfterCurrent and khDropInvSyncState == KH_DROP_SYNC_IDLE and next(khDropExpectedItems or {}) ~= nil then khDropSyncQueuedAfterCurrent = false khStartDropInventorySync('drop') end end if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() wait(delay or 250); run() end) else run() end return true end local function khReadDropInventoryLine(line) local cleanLine = clearNotifyText(line) local count = tonumber(cleanLine:match('%[(%d+)%s*шт%]') or cleanLine:match('%((%d+)%s*шт%.?%)') or 0) if count == 0 and cleanLine:match('%S') then count = 1 end local name = cleanLine:gsub('%[%d+%]%s*', ''):gsub('%s*%[%d+%s*шт%]', ''):gsub('%s*%(%d+%s*шт%.?%)', ''):gsub(' ', '') name = khCleanDropItemName(name:gsub('^%d+%.%s*', '')) local lowerName = khLowerCp1251(name) if name == '' or name:find('>>', 1, true) or lowerName == 'назад' or lowerName == 'закрыть' or lowerName:find('инвентарь', 1, true) then return end khDropTempInvCounts[lowerName] = (khDropTempInvCounts[lowerName] or 0) + (tonumber(count) or 1) end local function khApplyDropInventoryDelta() for safeName, data in pairs(khDropExpectedItems) do local oldCount = khDropInvCounts[safeName] or 0 local newCount = khDropTempInvCounts[safeName] or 0 local added = newCount - oldCount local fallback = math.max(1, math.floor((tonumber(data.fallback) or 1) + 0.5)) local count = nil if khDropInvReady and added > 0 then added = math.floor(added + 0.5) if added > math.max(fallback * 4, fallback + 25) then count = fallback else count = math.max(1, added) end elseif not khDropInvReady then count = fallback end if count ~= nil and count > 0 then khAddPendingDropItem(data.name, count) end end khDropExpectedItems = {} khDropRecentlyQueuedByAdded = {} end function khHandleDropStatsDialog(id, title, text) title = tostring(title or '') text = tostring(text or '') local syncActive = khDropInvSyncState == KH_DROP_SYNC_WAIT_STATS or khDropInvSyncState == KH_DROP_SYNC_SCANNING if not syncActive then if khDropStatsSilentUntil > os.clock() and khIsDropStatsDialog(title, text) then khCloseDropStatsDialog(id, 0) return false end return nil end khDropStatsHadDialog = true if khDropInvSyncState == KH_DROP_SYNC_WAIT_STATS then khDropInvSyncState = KH_DROP_SYNC_SCANNING khDropTempInvCounts = {} khDialogResponseLater(id, 1, 6, '', 60) return false elseif khDropInvSyncState == KH_DROP_SYNC_SCANNING then for line in text:gmatch('[^' .. string.char(13) .. string.char(10) .. ']+') do khReadDropInventoryLine(line) end if text:find('>>', 1, true) then khDialogResponseLater(id, 1, 40, '', 120) else if khDropInvSyncMode == 'drop' then khApplyDropInventoryDelta() end khDropInvCounts = khDropTempInvCounts khDropTempInvCounts = {} khDropInvReady = true khDropInvSyncMode = 'idle' khDropInvSyncState = KH_DROP_SYNC_IDLE khDropStatsSilentUntil = os.clock() + 2.0 khCloseDropStatsDialog(id, 60) khStartQueuedDropInventorySync(250) end return false end return nil end function khReportDig() if type(khAsyncHttpRequest) ~= 'function' then return end pcall(khAsyncHttpRequest, 'GET', 'http://138.124.127.115:27816/dig', {timeout = 6, kh_thread_timeout = 10}, function() end, function() end) end function khUpdatePendingDropSession(nowClock) if khPendingDropSession == nil then return end nowClock = tonumber(nowClock) or os.clock() local age = nowClock - (tonumber(khPendingDropSession.lastDropClock) or nowClock) if #khPendingDropSession.items > 0 and age >= 10.0 then local log = {id = khNextDropLogId(), timestamp = tonumber(khPendingDropSession.timestamp) or os.time(), date = khPendingDropSession.date or os.date('%Y-%m-%d'), time = khPendingDropSession.time or os.date('%H:%M:%S'), items = khPendingDropSession.items} table.insert(khDropLogs, log) khInvalidateDropProfitCache() khCurrentDropLogId = log.id khDropSelectedDate = log.date khDropSelectedLogId = log.id khPendingDropSession = nil khSaveDropStats() nakhodkaNotify(sName .. 'Клад вскопан полностью', -1, 'success', 3) khTyanCelebrate('klad') pcall(khReportDig) elseif #khPendingDropSession.items == 0 and age >= 25.0 then khPendingDropSession = nil end end local function khQueueDropSyncFromMessage(itemName, fallbackCount, fromAddedLine) itemName = khCleanDropItemName(itemName) if itemName == '' then return false end fallbackCount = math.max(1, math.floor((tonumber(fallbackCount) or 1) + 0.5)) local safeName = khLowerCp1251(itemName) local nowClock = os.clock() local recentAdded = khDropRecentlyQueuedByAdded[safeName] if fromAddedLine then khDropRecentlyQueuedByAdded[safeName] = nowClock + 3.0 elseif recentAdded ~= nil and recentAdded >= nowClock then local session = khEnsurePendingDropSession() session.lastDropClock = nowClock khLastDropAt = nowClock return true end local session = khEnsurePendingDropSession() session.lastDropClock = nowClock khLastDropAt = nowClock khQueueExpectedDropItem(itemName, fallbackCount) khQueueTreasureBoxExpected(session) if khDropInvSyncState == KH_DROP_SYNC_IDLE then if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() wait(500); khStartDropInventorySync('drop') end) else khStartDropInventorySync('drop') end else khDropSyncQueuedAfterCurrent = true end return true end function khHandlePreDigServerMessage(message) local text = clearNotifyText(message) local nick = text:match('^%s*([%w_%.]+)%[%d+%]%s+начал%s+что%-то%s+копать') if not nick or nick ~= (khGetMyNickname and khGetMyNickname() or '') then return false end local nowClock = os.clock() if nowClock - (tonumber(khDropLastPreDigSyncAt) or 0) < 2.0 then return false end khDropLastPreDigSyncAt = nowClock if khDropInvSyncState == KH_DROP_SYNC_IDLE then khStartDropInventorySync(khDropInvReady and 'pre_drop' or 'initial') end return true end local function khHandleDropServerMessage(message) local addedName, addedCount = khParseAddedItemMessage(message) if addedName then khRememberPendingAddedItem(addedName, addedCount or 1) return end local itemName, count = khParseDropMessage(message) if itemName then local fallbackCount = count or 1 local safeItemName = khLowerCp1251(itemName) local pendingItems = khTakeRecentPendingAddedItems(6.0, false) for _, pending in ipairs(pendingItems) do if khLowerCp1251(pending.name) == safeItemName then fallbackCount = tonumber(pending.count) or fallbackCount else khQueueDropSyncFromMessage(pending.name, pending.count, false) end end khQueueDropSyncFromMessage(itemName, fallbackCount, false) khDropPendingItemName, khDropPendingItemCount = nil, 1 return end if khIsOwnDopMessage(message) then local pendingItems = khTakeRecentPendingAddedItems(8.0, false) for _, pending in ipairs(pendingItems) do khQueueDropSyncFromMessage(pending.name, pending.count, false) end end end local khTabs = { { view = 'main', icon = 0xF8E2, name = 'Основное', title = 'Основное', desc = 'Главные настройки легальной версии.' }, { view = 'points', icon = 0xF3E8, name = 'Точки', title = 'Точки', desc = 'Список всех основных точек: ближайшие сверху, клик по строке ставит или снимает метку.' }, { view = 'stats', icon = 0xF2EE, name = 'Статистика', title = 'Дроп по дням', desc = 'Выбирай день, потом клад, справа будет список выпавших предметов и прибыль по заданным ценам.' }, { view = 'prices', icon = 0xF632, name = 'Цены', title = 'Цены', desc = 'Укажи цену предметов, которые падают с кладов. По ним считается прибыль в статистике и панели Статистика.' }, { view = 'dopki', icon = 0xF3E6, name = 'Допки', title = 'Окно допок', desc = 'Список дополнительных кладов: сортировка по дистанции, выбор допки и постановка метки.' }, { view = 'settings', icon = 0xF3E5, name = 'Настройки', title = 'Настройки', desc = '' }, { view = 'info', icon = 0xF431, name = 'Информация', title = 'Информация', desc = 'Что за скрипт и какие команды сейчас доступны.' } } local khCommands = { { tab = 9, cmd = '/nakhodka /kh', desc = '\xce\xf2\xea\xf0\xfb\xf2\xfc\x20\xfd\xf2\xee\x20\xec\xe5\xed\xfe\x2e' }, { tab = 1, cmd = '/zonecopy /zc', desc = '\xd1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xf2\xfc\x20\xf2\xe5\xea\xf3\xf9\xf3\xfe\x20\xe7\xee\xed\xf3\x20\xe2\x20\xe1\xf3\xf4\xe5\xf0\x2e' }, { tab = 1, cmd = '/zonepaste /zp', desc = '\xc2\xf1\xf2\xe0\xe2\xe8\xf2\xfc\x20\xe8\xeb\xe8\x20\xf3\xe1\xf0\xe0\xf2\xfc\x20\xe7\xee\xed\xf3\x20\xef\xee\x20\xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xe0\xec\x2e' }, { tab = 1, cmd = '/zonedel /zd', desc = '\xd3\xe4\xe0\xeb\xe8\xf2\xfc\x20\xe0\xea\xf2\xe8\xe2\xed\xf3\xfe\x20\xe7\xee\xed\xf3\x20\xf1\x20\xea\xe0\xf0\xf2\xfb\x2e' }, { tab = 1, cmd = '/zoneintersec N /zi N', desc = '\xcd\xe0\xe9\xf2\xe8\x20\xef\xe5\xf0\xe5\xf1\xe5\xf7\xe5\xed\xe8\xe5\x20\xef\xee\xf1\xeb\xe5\xe4\xed\xe8\xf5\x20\x4e\x20\xe7\xee\xed\x2e' }, { tab = 1, cmd = '/zonerestore N /zr N', desc = '\xc2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xe8\xf2\xfc\x20\xe7\xee\xed\xf3\x20\xe8\xe7\x20\xe8\xf1\xf2\xee\xf0\xe8\xe8\x2e' }, { tab = 2, cmd = '/metkacopy /mc', desc = '\xd1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xf2\xfc\x20\xf1\xe2\xee\xe8\x20\xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb\x20\xe4\xeb\xff\x20\xec\xe5\xf2\xea\xe8\x2e' }, { tab = 2, cmd = '/metkapaste /mp', desc = '\xcf\xee\xf1\xf2\xe0\xe2\xe8\xf2\xfc\x20\xec\xe5\xf2\xea\xf3\x20\xe8\x20\xf7\xe5\xea\xef\xee\xe8\xed\xf2\x3b\x20\xe1\xe5\xe7\x20\xe0\xf0\xe3\xf3\xec\xe5\xed\xf2\xee\xe2\x20\xf3\xe4\xe0\xeb\xff\xe5\xf2\x20\xe8\xf5\x2e' }, { tab = 3, cmd = '/iskd', desc = '\xcf\xf0\xee\xe2\xe5\xf0\xe8\xf2\xfc\x20\xea\xf3\xeb\xe4\xe0\xf3\xed\x20\xea\xe0\xf0\xf2\xfb\x2e' }, { tab = 4, cmd = '/delgangzones /dgz', desc = '\xd3\xe4\xe0\xeb\xe8\xf2\xfc\x20\xeb\xe8\xf8\xed\xe8\xe5\x20\x67\x61\x6e\x67\x20\x7a\x6f\x6e\x65\x73\x2e' } } local function toggleNakhodkaMenu() if khMenuState == nil then nakhodkaNotify('\x6d\x69\x6d\x67\x75\x69\x20\xed\xe5\x20\xed\xe0\xe9\xe4\xe5\xed\x2c\x20\xec\xe5\xed\xfe\x20\xed\xe5\xe4\xee\xf1\xf2\xf3\xef\xed\xee\x2e', -1, 'error') return end khMenuState[0] = not khMenuState[0] end local function khText(text) return u8(text) end khParticles = nil function khEnsureParticles() if khParticles ~= nil then return khParticles end if khParticlesLib == nil or imgui == nil then return nil end local pw, ph = 1920, 1080 if type(getScreenResolution) == 'function' then local okr, rw, rh = pcall(getScreenResolution); if okr and rw and rh then pw, ph = rw, rh end end local ar, ag, ab = 0.72, 0.45, 0.96 if khThemeCurrentRGB ~= nil then ar, ag, ab = khThemeCurrentRGB() end local ok, sys = pcall(function() return khParticlesLib:new({ max_particles = 300, max_distance = 200, gravity = 0, infinite_life = true, boundary_behavior = 'bounce', color = {ar, ag, ab, 0.85}, line_color = {ar, ag, ab, 0.45}, particle_size = 2, min_speed = 0.15, max_speed = 0.9, magnetism = 'none', size = imgui.ImVec2(pw, ph) }) end) if ok then khParticles = sys end return khParticles end khToggleState = {} function khToggle(label, boolVar) if imgui == nil or imgui.GetWindowDrawList == nil or imgui.InvisibleButton == nil or imgui.GetCursorScreenPos == nil then return imgui.Checkbox(label, boolVar) end local h, w = 18, 32 local draw = imgui.GetWindowDrawList() local pos = imgui.GetCursorScreenPos() local changed = imgui.InvisibleButton('##khtgl_' .. tostring(label), imgui.ImVec2(w, h)) if changed and boolVar ~= nil then boolVar[0] = not boolVar[0] end local on = boolVar ~= nil and boolVar[0] and true or false local key = tostring(label) local t = khToggleState[key] if t == nil then t = on and 1.0 or 0.0 end local io = imgui.GetIO() local dt = (io and io.DeltaTime) or 0.016 local target = on and 1.0 or 0.0 t = t + (target - t) * math.min(1, dt * 14) if math.abs(t - target) < 0.003 then t = target end khToggleState[key] = t local cr, cg, cb = 0.72, 0.45, 0.96 if khThemeCurrentRGB ~= nil then cr, cg, cb = khThemeCurrentRGB() end local oR, oG, oB = 0.30, 0.30, 0.36 local function u32(r, g, b, a) return imgui.ColorConvertFloat4ToU32(imgui.ImVec4(r, g, b, a)) end local bg = u32(oR + (cr - oR) * t, oG + (cg - oG) * t, oB + (cb - oB) * t, 1.0) local rad = h / 2 draw:AddRectFilled(pos, imgui.ImVec2(pos.x + w, pos.y + h), bg, rad) local knobX = pos.x + rad + t * (w - h) draw:AddCircleFilled(imgui.ImVec2(knobX, pos.y + rad), rad - 2.5, u32(1, 1, 1, 1)) if label ~= nil and label ~= '' then imgui.SameLine() imgui.Text(label) end return changed end function khUtf8Codepoint(cp) cp = tonumber(cp) or 0 if cp <= 0x7F then return string.char(cp) elseif cp <= 0x7FF then return string.char(0xC0 + math.floor(cp / 0x40), 0x80 + (cp % 0x40)) elseif cp <= 0xFFFF then return string.char(0xE0 + math.floor(cp / 0x1000), 0x80 + (math.floor(cp / 0x40) % 0x40), 0x80 + (cp % 0x40)) end return string.char(0xF0 + math.floor(cp / 0x40000), 0x80 + (math.floor(cp / 0x1000) % 0x40), 0x80 + (math.floor(cp / 0x40) % 0x40), 0x80 + (cp % 0x40)) end khBiIcons = { move = khUtf8Codepoint(0xF14E), map = khUtf8Codepoint(0xF47F), mapMarked = khUtf8Codepoint(0xF64B), today = khUtf8Codepoint(0xF291), total = khUtf8Codepoint(0xF5E6), profit = khUtf8Codepoint(0xF649), checkCircle = khUtf8Codepoint(0xF26A) } function khTyanSplitPhrases(text) local list = {} for part in tostring(text or ''):gmatch('[^|]+') do if part ~= '' then table.insert(list, part) end end return list end khTyanPhrases = {} khTyanCelebratePhrases = {} khMaratPhrases = {} khMaratCelebratePhrases = {} function khPhraseJsonText(value) local text = tostring(value or '') if text == '' then return '' end local ok, decoded = pcall(function() return u8:decode(text) end) if ok and decoded ~= nil and decoded ~= '' then return decoded end return text end function khPhraseListFromJson(value) local result = {} if type(value) ~= 'table' then return result end for _, phrase in ipairs(value) do local text = khPhraseJsonText(phrase) if text ~= '' then result[#result + 1] = text end end return result end function khWriteCompanionPhrasesJson() local file = io.open(khGetPhrasesPath(), 'wb') if not file then return false end file:write('{"version":1,"tyan":[],"tyan_celebrate":[],"marat":[],"marat_celebrate":[]}' .. string.char(10)) file:close() return true end function khLoadCompanionPhrases() local text = khReadTextFile(khGetPhrasesPath()) if text == nil or text == '' then khWriteCompanionPhrasesJson() else local data = khJsonDecodeConfig(text) if type(data) == 'table' then local tyan = khPhraseListFromJson(data.tyan) local tyanCelebrate = khPhraseListFromJson(data.tyan_celebrate or data.tyanCelebrate) local marat = khPhraseListFromJson(data.marat) local maratCelebrate = khPhraseListFromJson(data.marat_celebrate or data.maratCelebrate) if #tyan > 0 then khTyanPhrases = tyan end if #tyanCelebrate > 0 then khTyanCelebratePhrases = tyanCelebrate end if #marat > 0 then khMaratPhrases = marat end if #maratCelebrate > 0 then khMaratCelebratePhrases = maratCelebrate end else nakhodkaNotify('Не удалось загрузить nakhodka_phrases.json: JSON поврежден.', -1, 'error', 4) end end if #khTyanPhrases == 0 then khTyanPhrases = {'Я рядом, мой хороший.'} end if #khTyanCelebratePhrases == 0 then khTyanCelebratePhrases = {'Ты мой герой, любимый.'} end if #khMaratPhrases == 0 then khMaratPhrases = {'Двигаемся ровно, брат.'} end if #khMaratCelebratePhrases == 0 then khMaratCelebratePhrases = {'Красавчик, всё впереди.'} end end function khTyanCurrentPhrases() return khTyanVariant == 1 and khMaratPhrases or khTyanPhrases end function khTyanCurrentCelebratePhrases() return khTyanVariant == 1 and khMaratCelebratePhrases or khTyanCelebratePhrases end function khTyanHelloPhrase() return khTyanVariant == 1 and 'Спасибо хорошо' or 'Привет =)' end function khTyanMoveStartPhrase() return khTyanVariant == 1 and 'Передвинь меня куда удобно, я постою рядом.' or 'Аккуратно передвинь меня, я буду рядом.' end function khTyanMoveSavedPhrase() return khTyanVariant == 1 and 'Нормально, тут удобно стоять.' or 'Готово, я тихонько останусь здесь.' end function khTyanMoveSavedNotify() return khTyanVariant == 1 and 'Позиция Марата сохранена.' or 'Позиция тяночки сохранена.' end function khTyanApplyVariantNow(value, nowClock) local nextVariant = tonumber(value) == 1 and 1 or 0 if khTyanVariant ~= nextVariant then khTyanVariant = nextVariant khTyanText = '' khTyanWasEnabled = false khTyanScheduleNext((tonumber(nowClock) or os.clock()) + 0.6) end end function khTyanSetVariant(value) local nextVariant = tonumber(value) == 1 and 1 or 0 if khTyanPendingVariant == nextVariant or (khTyanPendingVariant == nil and khTyanVariant == nextVariant) then return false end khTyanPendingVariant = nil khTyanSwitchApplyAt = 0 khTyanApplyVariantNow(nextVariant, os.clock()) return true end function khTyanApplyPendingVariant(nowClock) if khTyanPendingVariant == nil then return false end nowClock = tonumber(nowClock) or os.clock() if nowClock < (khTyanSwitchApplyAt or 0) then return true end khTyanApplyVariantNow(khTyanPendingVariant, nowClock) khTyanPendingVariant = nil khTyanSwitchApplyAt = 0 khSaveMainSettings() return true end function khTyanPick(list) if type(list) ~= 'table' or #list == 0 then return '...' end return list[math.random(1, #list)] end function khTyanScheduleNext(nowClock) khTyanNextPhraseAt = (tonumber(nowClock) or os.clock()) + math.random(34, 92) end function khTyanSay(text, priority, holdSec) if khTyanEnabled == nil or not khTyanEnabled[0] then return end khTyanLoadingDots = false khTyanText = tostring(text or '') khTyanStartedAt = os.clock() if tonumber(holdSec) ~= nil then khTyanHoldSec = math.max(1.4, tonumber(holdSec)) else khTyanHoldSec = math.max(4.8, math.min(9.2, #khTyanText * 0.055 + 3.0)) end khTyanScheduleNext(khTyanStartedAt + khTyanHoldSec) end function khTyanStartLoadingDots() if khTyanEnabled == nil or not khTyanEnabled[0] then return end khTyanLoadingDots = true khTyanLoadingStartedAt = os.clock() khTyanText = '...' khTyanStartedAt = khTyanLoadingStartedAt khTyanHoldSec = 999 khTyanNextPhraseAt = khTyanLoadingStartedAt + 999 end function khTyanStopLoadingDots() if not khTyanLoadingDots then return end khTyanLoadingDots = false if tostring(khTyanText or '') == '...' then khTyanText = '' end end function khTyanCelebrate(kind) khTyanSay(khTyanPick(khTyanCurrentCelebratePhrases()), true) end function khTyanAssetPath(variant) local base = 'moonloader\\resource' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() .. '\\resource' end variant = tonumber(variant) == 1 and 1 or 0 local fileName = variant == 1 and 'nakhodka_marat_crop.png' or 'nakhodka_tyan.png' return base .. '\\' .. fileName end function khTyanEnsureTexture(variant) variant = tonumber(variant) == 1 and 1 or 0 if khTyanTextures ~= nil and khTyanTextures[variant] ~= nil then khTyanTexture = khTyanTextures[variant] khTyanTextureVariant = variant return khTyanTextures[variant] end if khTyanTextureTriedByVariant ~= nil and khTyanTextureTriedByVariant[variant] then return nil end if khTyanTextureTriedByVariant == nil then khTyanTextureTriedByVariant = {} end if khTyanTextures == nil then khTyanTextures = {} end khTyanTextureTriedByVariant[variant] = true if imgui ~= nil and type(imgui.CreateTextureFromFile) == 'function' then local ok, tex = pcall(imgui.CreateTextureFromFile, khTyanAssetPath(variant)) if ok and tex ~= nil then khTyanTextures[variant] = tex khTyanTexture = tex khTyanTextureVariant = variant end end return khTyanTextures[variant] end function khTyanU32(r, g, b, a) return imgui.ColorConvertFloat4ToU32(imgui.ImVec4(r, g, b, a)) end function khTyanClampPosition() local sw, sh = 1920, 1080 if type(getScreenResolution) == 'function' then local rw, rh = getScreenResolution() sw, sh = tonumber(rw) or sw, tonumber(rh) or sh end local windowW = math.max(360, khTyanSize + 230) local windowH = math.max(235, khTyanSize + 152) khTyanPos.x = math.max(0, math.min(tonumber(khTyanPos.x) or 240, sw - windowW)) khTyanPos.y = math.max(0, math.min(tonumber(khTyanPos.y) or 500, sh - windowH)) end function khTyanBubbleText(nowClock) local text = tostring(khTyanText or '') if text == '' then return '', 0 end if khTyanLoadingDots then return '...', 1 end local age = (tonumber(nowClock) or os.clock()) - (tonumber(khTyanStartedAt) or 0) local chars = math.max(1, math.floor(age / 0.035)) local shown = text if chars < #text then shown = text:sub(1, chars) end local alpha = 1 if age > khTyanHoldSec then alpha = 1 - ((age - khTyanHoldSec) / 1.60) end if alpha <= 0 then khTyanText = '' khTyanLoadingDots = false khTyanLoadingStartedAt = 0 khTyanScheduleNext(nowClock) return '', 0 end return shown, math.min(1, math.max(0, alpha)) end function khTyanBubbleLayout(text, windowW, tipLocalX) local minW = khTyanVariant == 1 and 154 or 158 local maxW = math.min(math.max(250, windowW - 18), 390) if khTyanLoadingDots then minW = 82 end local padX, padY = 15, 9 local rawW, rawH = 0, 18 if imgui ~= nil and imgui.CalcTextSize ~= nil then local ok, size = pcall(function() return imgui.CalcTextSize(khText(text)) end) if ok and size ~= nil then rawW = tonumber(size.x) or 0 rawH = tonumber(size.y) or rawH end end if rawW <= 0 then rawW = math.max(20, #tostring(text or '') * 7) end local textLen = #tostring(text or '') local preferredW = rawW + padX * 2 + 10 if preferredW > maxW then preferredW = math.min(maxW, math.max(250, math.sqrt(math.max(rawW, 1) * 230) + padX * 2)) end local bubbleW = math.max(minW, math.min(maxW, preferredW)) if khTyanLoadingDots then bubbleW = math.max(82, math.min(96, bubbleW)) end local textW = math.max(110, bubbleW - padX * 2) local linesByWidth = math.max(1, math.ceil(rawW / textW)) local linesByChars = math.max(1, math.ceil(textLen / 42)) local lines = math.max(linesByWidth, linesByChars) local lineH = math.max(18, rawH) local bubbleH = math.max(42, math.min(108, padY * 2 + lineH * lines + 6)) if khTyanLoadingDots then bubbleH = 40 end local bubbleX = math.max(6, math.min((tonumber(tipLocalX) or windowW * 0.5) - bubbleW * 0.5, windowW - bubbleW - 6)) return bubbleX, bubbleW, bubbleH, padX, padY end function khTyanRenderOverlay() if khTyanEnabled == nil or not khTyanEnabled[0] then khTyanWasEnabled = false return end local nowClock = os.clock() khTyanApplyPendingVariant(nowClock) if not khTyanWasEnabled then khTyanWasEnabled = true if khTyanText == '' then khTyanSay(khTyanHelloPhrase(), true, 2.8) end elseif khTyanText == '' and (khTyanNextPhraseAt == 0 or nowClock >= khTyanNextPhraseAt) then khTyanSay(khTyanPick(khTyanCurrentPhrases()), false) end khTyanClampPosition() local windowW = math.max(360, khTyanSize + 230) local windowHBase = math.max(235, khTyanSize + 152) local bubbleTopExtra = 116 local windowY = math.max(0, (tonumber(khTyanPos.y) or 0) - bubbleTopExtra) local actualTopExtra = (tonumber(khTyanPos.y) or 0) - windowY local windowH = windowHBase + actualTopExtra local flags = 0 if imgui.WindowFlags.NoTitleBar ~= nil then flags = flags + imgui.WindowFlags.NoTitleBar end if imgui.WindowFlags.NoResize ~= nil then flags = flags + imgui.WindowFlags.NoResize end if imgui.WindowFlags.NoScrollbar ~= nil then flags = flags + imgui.WindowFlags.NoScrollbar end if imgui.WindowFlags.NoSavedSettings ~= nil then flags = flags + imgui.WindowFlags.NoSavedSettings end if imgui.WindowFlags.NoCollapse ~= nil then flags = flags + imgui.WindowFlags.NoCollapse end if imgui.WindowFlags.NoFocusOnAppearing ~= nil then flags = flags + imgui.WindowFlags.NoFocusOnAppearing end if not khTyanMoveMode and imgui.WindowFlags.NoInputs ~= nil then flags = flags + imgui.WindowFlags.NoInputs end if imgui.WindowFlags.NoBackground ~= nil then flags = flags + imgui.WindowFlags.NoBackground end imgui.SetNextWindowPos(imgui.ImVec2(khTyanPos.x, windowY), imgui.Cond.Always) imgui.SetNextWindowSize(imgui.ImVec2(windowW, windowH), imgui.Cond.Always) imgui.PushStyleVarVec2(imgui.StyleVar.WindowPadding, imgui.ImVec2(0, 0)) imgui.PushStyleVarFloat(imgui.StyleVar.WindowRounding, 0) if imgui.Begin('kh_tyan##assistant', nil, flags) then local draw = imgui.GetWindowDrawList() local win = imgui.GetWindowPos() local imgX = math.floor((windowW - khTyanSize) * 0.5) local imgY = 80 + actualTopExtra local text, alpha = khTyanBubbleText(nowClock) local tex = khTyanEnsureTexture(khTyanVariant) if tex ~= nil and draw ~= nil and draw.AddImage ~= nil then draw:AddImage(tex, imgui.ImVec2(win.x + imgX, win.y + imgY), imgui.ImVec2(win.x + imgX + khTyanSize, win.y + imgY + khTyanSize), imgui.ImVec2(0, 0), imgui.ImVec2(1, 1), khTyanU32(1, 1, 1, 1)) elseif draw ~= nil then draw:AddRectFilled(imgui.ImVec2(win.x + imgX, win.y + imgY), imgui.ImVec2(win.x + imgX + khTyanSize, win.y + imgY + khTyanSize), khThemeColorU32(nil, 'child', 0.92), 20) draw:AddRect(imgui.ImVec2(win.x + imgX, win.y + imgY), imgui.ImVec2(win.x + imgX + khTyanSize, win.y + imgY + khTyanSize), khThemeColorU32(nil, 'bubbleBorder', 0.95), 20, 0, 2) end if khTyanMoveMode and draw ~= nil then draw:AddRect(imgui.ImVec2(win.x + imgX - 3, win.y + imgY - 3), imgui.ImVec2(win.x + imgX + khTyanSize + 3, win.y + imgY + khTyanSize + 3), khThemeColorU32(nil, 'accentText', 0.95), 18, 0, 2.2) end if alpha > 0 and text ~= '' then local bg = khThemeColorU32(nil, 'bubbleBg', 0.90 * alpha) local border = khThemeColorU32(nil, 'bubbleBorder', 0.84 * alpha) local textCol = khThemeColor(nil, 'text', alpha) local tailHalfOuter = khTyanVariant == 1 and 14 or 12 local tailHalfInner = khTyanVariant == 1 and 10 or 8 local tailOuter = khTyanVariant == 1 and 16 or 9 local tailInner = khTyanVariant == 1 and 12 or 6 local tipLocalX = imgX + khTyanSize * 0.5 local bubbleLocalX, bubbleW, bubbleH, padX, padY = khTyanBubbleLayout(text, windowW, tipLocalX) local bubbleGap = khTyanVariant == 1 and 12 or 9 local bubbleBottom = imgY - bubbleGap - tailOuter local bubbleY = math.max(4, bubbleBottom - bubbleH) local x1, y1 = win.x + bubbleLocalX, win.y + bubbleY local x2, y2 = x1 + bubbleW, y1 + bubbleH local tipX = win.x + tipLocalX draw:AddRectFilled(imgui.ImVec2(x1, y1), imgui.ImVec2(x2, y2), border, 18) draw:AddTriangleFilled(imgui.ImVec2(tipX - tailHalfOuter, y2 - 1), imgui.ImVec2(tipX + tailHalfOuter, y2 - 1), imgui.ImVec2(tipX, y2 + tailOuter), border) draw:AddRectFilled(imgui.ImVec2(x1 + 2, y1 + 2), imgui.ImVec2(x2 - 2, y2 - 2), bg, 16) draw:AddTriangleFilled(imgui.ImVec2(tipX - tailHalfInner, y2 - 2), imgui.ImVec2(tipX + tailHalfInner, y2 - 2), imgui.ImVec2(tipX, y2 + tailInner), bg) if khTyanLoadingDots and draw.AddCircleFilled ~= nil then local dotBaseX = x1 + bubbleW * 0.5 - 16 local dotBaseY = y1 + bubbleH * 0.5 + 1 for dotIndex = 1, 3 do local phase = (nowClock - (tonumber(khTyanLoadingStartedAt) or nowClock)) * 5.4 + dotIndex * 0.95 local dy = math.sin(phase) * 3.5 local dotAlpha = (0.62 + 0.38 * math.sin(phase + 0.7)) * alpha draw:AddCircleFilled(imgui.ImVec2(dotBaseX + (dotIndex - 1) * 16, dotBaseY + dy), 4.2, khThemeColorU32(nil, 'accentText', dotAlpha), 12) end else imgui.SetCursorPos(imgui.ImVec2(bubbleLocalX + padX, bubbleY + padY)) imgui.PushTextWrapPos(bubbleLocalX + bubbleW - padX) if imgui.TextWrapped ~= nil and imgui.PushStyleColor ~= nil and imgui.PopStyleColor ~= nil then imgui.PushStyleColor(imgui.Col.Text, textCol) imgui.TextWrapped(khText(text)) imgui.PopStyleColor() else imgui.TextColored(textCol, khText(text)) end imgui.PopTextWrapPos() end end imgui.SetCursorPos(imgui.ImVec2(imgX, imgY)) imgui.InvisibleButton('##kh_tyan_drag_area', imgui.ImVec2(khTyanSize, khTyanSize)) local io = imgui.GetIO() if khTyanMoveMode then if khTyanMoveSaveLatch then if io == nil or io.MouseDown == nil or not io.MouseDown[0] then khTyanMoveSaveLatch = false end elseif imgui.IsItemHovered() and imgui.IsMouseClicked(0) and io and io.MousePos then khTyanDragging = true khTyanDragOffset.x = io.MousePos.x - khTyanPos.x khTyanDragOffset.y = io.MousePos.y - khTyanPos.y end if khTyanDragging then if io and io.MouseDown and io.MouseDown[0] and io.MousePos then khTyanPos.x = io.MousePos.x - khTyanDragOffset.x khTyanPos.y = io.MousePos.y - khTyanDragOffset.y else khTyanDragging = false khTyanMoveMode = false khTyanClampPosition() khSaveMainSettings() khTyanSay(khTyanMoveSavedPhrase(), true, 2.2) nakhodkaNotify(khTyanMoveSavedNotify(), -1, 'success', 2) end end else khTyanDragging = false end end imgui.End() imgui.PopStyleVar(2) end local function khHelp(text) imgui.SameLine() imgui.TextColored(khThemeColor(nil, 'muted'), '(?)') if imgui.IsItemHovered ~= nil and imgui.IsItemHovered() then if imgui.BeginTooltip ~= nil and imgui.EndTooltip ~= nil then imgui.BeginTooltip() if imgui.PushTextWrapPos ~= nil then imgui.PushTextWrapPos(360) end imgui.TextWrapped(khText(text)) if imgui.PopTextWrapPos ~= nil then imgui.PopTextWrapPos() end imgui.EndTooltip() elseif imgui.SetTooltip ~= nil then imgui.SetTooltip(khText(text)) end end end khClampFloat = function(value, default, minValue, maxValue) value = tonumber(value) or default if value < minValue then value = minValue end if value > maxValue then value = maxValue end return value end local function khPushStyle() local theme = khThemeCurrent() imgui.PushStyleVarFloat(imgui.StyleVar.WindowRounding, 16) imgui.PushStyleVarFloat(imgui.StyleVar.ChildRounding, 14) imgui.PushStyleVarFloat(imgui.StyleVar.FrameRounding, 11) imgui.PushStyleVarVec2(imgui.StyleVar.WindowPadding, imgui.ImVec2(14, 12)) imgui.PushStyleVarVec2(imgui.StyleVar.ItemSpacing, imgui.ImVec2(12, 10)) imgui.PushStyleColor(imgui.Col.WindowBg, khThemeColor(theme, 'window')) imgui.PushStyleColor(imgui.Col.ChildBg, khThemeColor(theme, 'child')) imgui.PushStyleColor(imgui.Col.Border, khThemeColor(theme, 'border')) imgui.PushStyleColor(imgui.Col.Text, khThemeColor(theme, 'text')) imgui.PushStyleColor(imgui.Col.Button, khThemeColor(theme, 'button')) imgui.PushStyleColor(imgui.Col.ButtonHovered, khThemeColor(theme, 'buttonHovered')) imgui.PushStyleColor(imgui.Col.ButtonActive, khThemeColor(theme, 'buttonActive')) imgui.PushStyleColor(imgui.Col.Header, khThemeColor(theme, 'header')) imgui.PushStyleColor(imgui.Col.HeaderHovered, khThemeColor(theme, 'headerHovered')) imgui.PushStyleColor(imgui.Col.HeaderActive, khThemeColor(theme, 'headerActive')) imgui.PushStyleColor(imgui.Col.FrameBg, khThemeColor(theme, 'frame')) imgui.PushStyleColor(imgui.Col.FrameBgHovered, khThemeColor(theme, 'frameHovered')) imgui.PushStyleColor(imgui.Col.FrameBgActive, khThemeColor(theme, 'frameActive')) imgui.PushStyleColor(imgui.Col.CheckMark, khThemeColor(theme, 'checkMark')) imgui.PushStyleColor(imgui.Col.SliderGrab, khThemeColor(theme, 'sliderGrab')) imgui.PushStyleColor(imgui.Col.SliderGrabActive, khThemeColor(theme, 'sliderGrabActive')) imgui.PushStyleColor(imgui.Col.ScrollbarBg, khThemeColor(theme, 'scrollbarBg')) imgui.PushStyleColor(imgui.Col.ScrollbarGrab, khThemeColor(theme, 'scrollbarGrab')) imgui.PushStyleColor(imgui.Col.ScrollbarGrabHovered, khThemeColor(theme, 'scrollbarGrabHovered')) imgui.PushStyleColor(imgui.Col.ScrollbarGrabActive, khThemeColor(theme, 'scrollbarGrabActive')) end local function khPopStyle() imgui.PopStyleColor(20) imgui.PopStyleVar(5) end local function khRenderTabButton(index) local tab = khTabs[index] local active = khTargetTab == index local theme = khThemeCurrent() if active then imgui.PushStyleColor(imgui.Col.Button, khThemeColor(theme, 'buttonActive')) imgui.PushStyleColor(imgui.Col.ButtonHovered, khThemeColor(theme, 'buttonHovered')) imgui.PushStyleColor(imgui.Col.ButtonActive, khThemeColor(theme, 'button')) else imgui.PushStyleColor(imgui.Col.Button, khThemeColor(theme, 'tabIdle')) imgui.PushStyleColor(imgui.Col.ButtonHovered, khThemeColor(theme, 'tabIdleHovered')) imgui.PushStyleColor(imgui.Col.ButtonActive, khThemeColor(theme, 'tabIdleActive')) end local remaining = math.max(1, #khTabs - index + 1) local avail = imgui.GetContentRegionAvail() local spacing = 8 local style = imgui.GetStyle() if style and style.ItemSpacing then spacing = style.ItemSpacing.y or spacing end local height = math.max(38, math.min(58, (avail.y - spacing * (remaining - 1)) / remaining)) local width = math.max(1, avail.x) local pos = imgui.GetCursorScreenPos() if imgui.Button('##kh_tab_' .. tostring(index), imgui.ImVec2(-1, height)) then if khTargetTab ~= index then khTargetTab = index local nowClock = os.clock() if (kh3DNativePauseUntil or 0) < nowClock + 0.25 then kh3DNativePauseUntil = nowClock + 0.25 end end end local draw = imgui.GetWindowDrawList() if draw ~= nil and imgui.ColorConvertFloat4ToU32 ~= nil then local icon = tab.icon and khUtf8Codepoint(tab.icon) or '' local title = khText(tab.name) local iconSize = icon ~= '' and imgui.CalcTextSize(icon) or imgui.ImVec2(0, 0) local titleSize = imgui.CalcTextSize(title) local gap = icon ~= '' and 9 or 0 local iconBox = icon ~= '' and 22 or 0 local totalWidth = iconBox + gap + titleSize.x local startX = pos.x + math.max(0, (width - totalWidth) * 0.5) local titleY = math.floor(pos.y + math.max(0, (height - titleSize.y) * 0.5)) local iconY = titleY + 2 local iconX = startX + math.max(0, math.floor((iconBox - iconSize.x) * 0.5)) local color = active and khThemeColor(theme, 'accentText') or khThemeColor(theme, 'mutedText') local colorU32 = imgui.ColorConvertFloat4ToU32(color) if icon ~= '' then draw:AddText(imgui.ImVec2(iconX, iconY), colorU32, icon) end draw:AddText(imgui.ImVec2(startX + iconBox + gap, titleY), colorU32, title) end imgui.PopStyleColor(3) end local function khRenderCommands(tabIndex) imgui.Columns(2, '##kh_commands_columns', false) imgui.SetColumnWidth(0, 185) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xca\xee\xec\xe0\xed\xe4\xe0')) imgui.NextColumn() imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xce\xef\xe8\xf1\xe0\xed\xe8\xe5')) imgui.NextColumn() imgui.Separator() for _, item in ipairs(khCommands) do if tabIndex == 8 or item.tab == tabIndex then imgui.TextColored(khThemeColor(nil, 'accentText'), item.cmd) imgui.NextColumn() imgui.TextWrapped(khText(item.desc)) imgui.NextColumn() end end imgui.Columns(1) end local function khRenderEmptySection() imgui.BeginChild('##kh_empty_section', imgui.ImVec2(0, 88), true) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xd0\xe0\xe7\xe4\xe5\xeb\x20\xe2\x20\xf0\xe0\xe7\xf0\xe0\xe1\xee\xf2\xea\xe5')) imgui.TextWrapped(khText('\xdd\xf2\xee\xf2\x20\xef\xf3\xed\xea\xf2\x20\xef\xee\xea\xe0\x20\xee\xf1\xf2\xe0\xe2\xeb\xe5\xed\x20\xef\xf3\xf1\xf2\xfb\xec\x2e\x20\xc4\xee\xe4\xe5\xeb\xe0\xe5\xec\x20\xe5\xe3\xee\x20\xef\xee\xe7\xe6\xe5\x2e')) imgui.EndChild() end function khRenderMainSettingsWindow() local noScrollFlags = 0 if imgui.WindowFlags.NoScrollbar ~= nil then noScrollFlags = noScrollFlags + imgui.WindowFlags.NoScrollbar end if imgui.WindowFlags.NoScrollWithMouse ~= nil then noScrollFlags = noScrollFlags + imgui.WindowFlags.NoScrollWithMouse end local avail = imgui.GetContentRegionAvail() local gap = 10 local hudHeight = math.max(104, math.floor((avail.y - gap) * 0.20)) local mainHeight = math.max(390, avail.y - hudHeight - gap) if mainHeight + hudHeight + gap > avail.y then mainHeight = math.max(350, avail.y - hudHeight - gap) end local accent = khThemeColor(nil, 'accentText') local function sectionTitle(text) imgui.TextColored(accent, khText(text)) imgui.Separator() end local function sliderRow(label, id, value, minValue, maxValue, helpText) local rowY = imgui.GetCursorPosY() imgui.SetCursorPosY(rowY + 3) imgui.Text(khText(label)) khHelp(helpText) imgui.SameLine() imgui.SetCursorPosY(rowY) imgui.SetCursorPosX(265) imgui.PushItemWidth(-10) local changed = imgui.SliderInt(id, value, minValue, maxValue) imgui.PopItemWidth() imgui.SetCursorPosY(rowY + 31) if changed then khSaveMainSettings() end return changed end imgui.PushStyleVarVec2(imgui.StyleVar.ItemSpacing, imgui.ImVec2(12, 6)) imgui.BeginChild('##kh_main_settings', imgui.ImVec2(0, mainHeight), true, noScrollFlags) sectionTitle('Главные настройки') imgui.Columns(2, '##kh_main_toggles', false) imgui.SetColumnWidth(0, math.max(360, avail.x * 0.50)) if khToggle(khText('Включить скрипт'), khScriptEnabled) then khSaveMainSettings() khApplyScriptEnabledState() end khHelp('Полностью включает или выключает функционал Nakhodka Legal: зоны, метки, допки, статистику и HUD.') imgui.NextColumn() if khToggle(khText('Только в пределах зоны'), khOnlyInZone) then khSaveMainSettings() khRefreshMainTreasureBlips(true) end khHelp('Основные метки показываются только внутри активной зоны клада. Когда зона не активна, метки скрываются.') imgui.Columns(1) if khToggle(khText('Операции с зоной'), khZoneOpsEnabled) then khSaveMainSettings() end khHelp('Включает или выключает работу скрипта с зоной: перехват, восстановление и команды зон (/zr, /zi, /zc, /zp, /zd).') if khToggle(khText('Спутник'), khTyanEnabled) then if khTyanEnabled[0] then khTyanSay(khTyanHelloPhrase(), true, 2.8) else khTyanText = '' khTyanWasEnabled = false khTyanMoveMode = false khTyanDragging = false khTyanMoveSaveLatch = false end khSaveMainSettings() end khHelp('Показывает выбранного спутника с облачком фраз. В обычном режиме он закреплен.') local versionRowY = imgui.GetCursorPosY() imgui.SetCursorPosY(versionRowY + 5) imgui.Text(khText('Версия спутника')) khHelp('Выбери, кто будет отображаться на экране: тяночка или Марат.') imgui.SameLine() imgui.SetCursorPosY(versionRowY) imgui.SetCursorPosX(265) local companionButtonW = math.max(120, math.floor((imgui.GetContentRegionAvail().x - 10) / 2)) local shownVariant = khTyanPendingVariant ~= nil and khTyanPendingVariant or khTyanVariant if shownVariant == 0 then imgui.PushStyleColor(imgui.Col.Button, accent) end if imgui.Button(khText('Тяночка') .. '##kh_companion_tyan', imgui.ImVec2(companionButtonW, 30)) then if khTyanSetVariant(0) then khSaveMainSettings() end end if shownVariant == 0 then imgui.PopStyleColor() end imgui.SameLine() if shownVariant == 1 then imgui.PushStyleColor(imgui.Col.Button, accent) end if imgui.Button(khText('Марат') .. '##kh_companion_marat', imgui.ImVec2(companionButtonW, 30)) then if khTyanSetVariant(1) then khSaveMainSettings() end end if shownVariant == 1 then imgui.PopStyleColor() end imgui.SetCursorPosY(versionRowY + 34) if imgui.Button(khText('Изменить положение'), imgui.ImVec2(math.min(300, imgui.GetContentRegionAvail().x), 30)) then khTyanEnabled[0] = true khTyanMoveMode = true khTyanDragging = false khTyanMoveSaveLatch = true khTyanSay(khTyanMoveStartPhrase(), true, 2.8) khSaveMainSettings() end khHelp('Включает режим перемещения: зажми спутника ЛКМ, перетащи и отпусти. Без этой кнопки он не двигается.') local sizeRowY = imgui.GetCursorPosY() imgui.SetCursorPosY(sizeRowY + 2) imgui.Text(khText('Размер спутника')) khHelp('Меняет размер картинки спутника на экране.') imgui.SameLine() imgui.SetCursorPosY(sizeRowY) imgui.SetCursorPosX(265) imgui.PushItemWidth(-10) if imgui.SliderInt('##kh_tyan_size', khTyanSizeInput, 80, 400, '') then local nextSize = math.floor((tonumber(khTyanSizeInput[0]) or khTyanSize or 136) + 0.5) if nextSize < 80 then nextSize = 80 end if nextSize > 400 then nextSize = 400 end khTyanSize = nextSize khTyanSizeInput[0] = khTyanSize khSaveMainSettings() end imgui.PopItemWidth() imgui.SetCursorPosY(sizeRowY + 29) imgui.Dummy(imgui.ImVec2(0, 2)) imgui.Separator() imgui.Dummy(imgui.ImVec2(0, 3)) if sliderRow('Радиус отображения меток', '##kh_point_display_radius', khPointDisplayRadius, 50, 2000, 'На каком расстоянии от игрока показывать основные точки на карте.') then khRefreshMainTreasureBlips(false) end if sliderRow('Иконка точки на карте', '##kh_point_icon', khPointIcon, 1, 63, 'Иконка основных кладов на миникарте. По умолчанию 56.') then khRefreshMainTreasureBlips(true) end imgui.Dummy(imgui.ImVec2(0, 3)) if imgui.Button(khText('Перезагрузить точки'), imgui.ImVec2(-1, 30)) then khReloadMainTreasureState() end imgui.EndChild() imgui.BeginChild('##kh_hud_settings', imgui.ImVec2(0, hudHeight), true, noScrollFlags) sectionTitle('Вспомогательная панель') imgui.Columns(2, '##kh_hud_columns', false) imgui.SetColumnWidth(0, math.max(360, avail.x * 0.50)) if khToggle(khText('Показать статистику'), khDropHudEnabled) then if not khDropHudEnabled[0] then khHudMoveMode = false khHudSaveLatch = false end khSaveMainSettings() end khHelp('Показывает маленькую статистику: прибыль, дроп сегодня, общий дроп и статус зоны.') imgui.NextColumn() local controlsWidth = imgui.GetContentRegionAvail().x local buttonWidth = math.max(1, math.floor((controlsWidth - 10) / 2)) if imgui.Button(khText('Настроить положение'), imgui.ImVec2(buttonWidth, 36)) then if khDropHudEnabled ~= nil then khDropHudEnabled[0] = true end khHudMoveMode = true khHudMoveArmed = false khHudSaveLatch = true khSaveMainSettings() end imgui.SameLine() if imgui.Button(khText('Сбросить позицию'), imgui.ImVec2(buttonWidth, 36)) then khResetHudPosition() nakhodkaNotify('Позиция Статистики сброшена.', -1, 'success', 2) end imgui.Columns(1) imgui.EndChild() imgui.PopStyleVar() end function khRenderTeamWindow() khRenderEmptySection() end function khRender3DMarkersWindow() khRenderEmptySection() end local function khBuildSortedMainPointList() local now = os.clock() if khMainPointListCacheAt > 0 and now - khMainPointListCacheAt < 1.0 then return khMainPointListCache end local px, py, pz = getCharCoordinates(PLAYER_PED) local result = {} for index, point in ipairs(khMainTreasurePoints) do local dist = 0 if px and py then dist = khDistance(px, py, pz or 0, point[1], point[2], point[3] or 0) end table.insert(result, { index = index, distance = dist }) end table.sort(result, function(a, b) return a.distance < b.distance end) if not khShowAllMainPointRows then while #result > khMaxMainPointRows do result[#result] = nil end end khMainPointListCache = result khMainPointListCacheAt = now return result end local function khRenderMainPointsWindow() local sorted = khBuildSortedMainPointList() -- \xc7\xe0\xf5\xe2\xe0\xf2\xfb\xe2\xe0\xe5\xec \xe8\xed\xe4\xe5\xea\xf1 \xe2 \xeb\xee\xea\xe0\xeb\xfc\xed\xf3\xfe \xef\xe5\xf0\xe5\xec\xe5\xed\xed\xf3\xfe: \xef\xf0\xe8 \xe1\xfb\xf1\xf2\xf0\xee\xec \xeb\xe8\xf1\xf2\xe0\xed\xe8\xe8 \xe3\xeb\xee\xe1\xe0\xeb\xfc\xed\xfb\xe9 \xee\xed -- \xec\xee\xe6\xe5\xf2 \xee\xe1\xed\xf3\xeb\xe8\xf2\xfc\xf1\xff (khClearMainTreasureMarker) \xec\xe5\xe6\xe4\xf3 \xef\xf0\xee\xe2\xe5\xf0\xea\xee\xe9 \xe8 string.format. local activeIndex = khActiveMainPointIndex local selectedPoint = activeIndex and khMainTreasurePoints[activeIndex] or nil imgui.BeginChild('##kh_main_points_list', imgui.ImVec2(465, 0), true) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('Список')) imgui.SameLine() imgui.TextColored(khThemeColor(nil, 'muted'), string.format('%d', #khMainTreasurePoints)) imgui.Separator() -- Рендерим все точки (их может быть 4000+), но рисуем только видимые строки -- через ImGuiListClipper — иначе тысячи Selectable за кадр убьют FPS. local count = #sorted local function khDrawMainPointRow(i) local entry = sorted[i] if not entry then return end local point = khMainTreasurePoints[entry.index] if not point then return end local label = string.format('Точка %d##kh_main_point_%d', entry.index, entry.index) if imgui.Selectable(khText(label), activeIndex == entry.index) then khToggleMainPointMarker(entry.index) end end local clipped = false if imgui.ImGuiListClipper ~= nil then local okClip = pcall(function() local clipper = imgui.ImGuiListClipper() clipper:Begin(count) while clipper:Step() do for i = clipper.DisplayStart + 1, clipper.DisplayEnd do khDrawMainPointRow(i) end end clipper:End() end) clipped = okClip end if not clipped then -- Запасной вариант без клиппера: ручная отсечка по видимой области скролла. local lineH = imgui.GetTextLineHeightWithSpacing() if not lineH or lineH <= 0 then lineH = 18 end local scrollY = imgui.GetScrollY() local winH = imgui.GetWindowHeight() local first = math.max(1, math.floor(scrollY / lineH) - 2) local visibleCount = math.floor(winH / lineH) + 4 local last = math.min(count, first + visibleCount) if first > 1 then imgui.Dummy(imgui.ImVec2(1, (first - 1) * lineH)) end for i = first, last do khDrawMainPointRow(i) end if last < count then imgui.Dummy(imgui.ImVec2(1, (count - last) * lineH)) end end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_main_points_info', imgui.ImVec2(0, 0), true) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('Метка')) imgui.Separator() if selectedPoint and activeIndex then imgui.Text(khText(string.format('Точка %d', activeIndex))) imgui.Separator() if imgui.Button(khText('Снять метку'), imgui.ImVec2(160, 30)) then khToggleMainPointMarker(activeIndex) end else imgui.TextColored(khThemeColor(nil, 'muted'), khText('Метка не выбрана.')) end imgui.EndChild() end local function khRenderDropStatsWindow() local days = khGetDropDays() local selectedDateExists = false for _, day in ipairs(days) do if tostring(day) == tostring(khDropSelectedDate or '') then selectedDateExists = true break end end if not selectedDateExists then khDropSelectedDate = nil khDropSelectedLogId = nil end local clearButtonW = 145 local topSize = imgui.GetWindowSize() local topY = imgui.GetCursorPosY() imgui.SetCursorPosX(math.max(0, topSize.x - clearButtonW - 20)) if imgui.Button(khText('Очистить лог'), imgui.ImVec2(clearButtonW, 28)) then khDropLogs = {} khDropPrices = {} khPriceInputs = {} khInvalidateAveragePriceCache() khDropSelectedDate = nil khDropSelectedLogId = nil khCurrentDropLogId = nil khPendingDropSession = nil khInvalidateDropProfitCache() khSaveDropStats() khSaveDropPrices() nakhodkaNotify('История дропа и цены очищены.', -1, 'success', 3) end imgui.SetCursorPosY(topY + 34) local logs = (khDropSelectedDate ~= nil and tostring(khDropSelectedDate) ~= '') and khGetDropLogsForDay(khDropSelectedDate) or {} if khDropSelectedDate ~= nil and (khDropSelectedLogId == nil or khGetDropLogById(khDropSelectedLogId) == nil) and #logs > 0 then khDropSelectedLogId = logs[1].id end local selectedLog = khGetDropLogById(khDropSelectedLogId) local noScrollFlags = 0 if imgui.WindowFlags.NoScrollbar ~= nil then noScrollFlags = noScrollFlags + imgui.WindowFlags.NoScrollbar end if imgui.WindowFlags.NoScrollWithMouse ~= nil then noScrollFlags = noScrollFlags + imgui.WindowFlags.NoScrollWithMouse end imgui.BeginChild('##kh_drop_days', imgui.ImVec2(170, 0), true, 0) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xc4\xed\xe8')) imgui.Separator() if #days == 0 then imgui.TextWrapped(khText('\xcf\xee\xea\xe0\x20\xed\xe5\xf2\x20\xf1\xf2\xe0\xf2\xe8\xf1\xf2\xe8\xea\xe8\x2e')) else for _, day in ipairs(days) do if imgui.Selectable(day, khDropSelectedDate == day) then khDropSelectedDate = day local dayLogs = khGetDropLogsForDay(day) khDropSelectedLogId = dayLogs[1] and dayLogs[1].id or nil end end end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_drop_logs', imgui.ImVec2(225, 0), true, 0) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xca\xeb\xe0\xe4\xfb')) imgui.Separator() for displayIndex, log in ipairs(logs) do local dayNumber = #logs - displayIndex + 1 local label = string.format('\xca\xeb\xe0\xe4\x20\x23\x25\x64 %s - %d предм.##kh_log_%d', dayNumber, tostring(log.time or ''), khDropLogItemCount(log), log.id) if imgui.Selectable(khText(label), tostring(khDropSelectedLogId) == tostring(log.id)) then khDropSelectedLogId = log.id end end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_drop_items', imgui.ImVec2(0, 0), true, 0) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xcf\xf0\xe5\xe4\xec\xe5\xf2\xfb')) if khDropSelectedDate ~= nil and tostring(khDropSelectedDate) ~= '' then imgui.Text(khText(string.format('Кладов за день: %d', #logs))) imgui.Text(khText(string.format('За день: %s VC', khFormatNumber(khGetDayDropProfit(khDropSelectedDate))))) else imgui.Text(khText(string.format('Всего кладов: %d', khGetTotalDropCount()))) imgui.Text(khText(string.format('Всего поднято: %s VC', khFormatNumber(khGetDropProfit())))) end imgui.Separator() if selectedLog and type(selectedLog.items) == 'table' and #selectedLog.items > 0 then imgui.Text(string.format('%s %s', tostring(selectedLog.date or ''), tostring(selectedLog.time or ''))) imgui.Text(khText(string.format('Прибыль: %s VC', khFormatNumber(khGetDropProfit(selectedLog))))) imgui.Separator() for _, item in ipairs(selectedLog.items) do imgui.TextWrapped(khText(string.format('\x25\x73\x20\x2d\x20\x25\x64\x20\xf8\xf2\x2e', tostring(item.name), tonumber(item.count) or 1))) end elseif khDropSelectedDate ~= nil and tostring(khDropSelectedDate) ~= '' then imgui.TextWrapped(khText('\xc2\xfb\xe1\xe5\xf0\xe8\x20\xea\xeb\xe0\xe4\x20\xf1\xeb\xe5\xe2\xe0\x2e')) else imgui.TextWrapped(khText('Выбери день слева, чтобы посмотреть клады за конкретную дату.')) end imgui.EndChild() end function khGetPriceInput(name) local key = tostring(name or '') local value = khGetDropItemPrice(key) if not imguiReady or new == nil then return {[0] = value} end if khPriceInputs[key] == nil then khPriceInputs[key] = new.int(value) end return khPriceInputs[key] end function khRenderPricesWindow() local items = khGetUniqueDropItems() local noScrollFlags = 0 if imgui.WindowFlags.NoScrollbar ~= nil then noScrollFlags = noScrollFlags + imgui.WindowFlags.NoScrollbar end if imgui.WindowFlags.NoScrollWithMouse ~= nil then noScrollFlags = noScrollFlags + imgui.WindowFlags.NoScrollWithMouse end imgui.BeginChild('##kh_prices_summary', imgui.ImVec2(0, 96), true, noScrollFlags) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('Итог')) imgui.Text(khText(string.format('Всего поднято: %s VC', khFormatNumber(khGetDropProfit())))) imgui.Text(khText(string.format('Предметов в списке: %d', #items))) imgui.EndChild() imgui.BeginChild('##kh_prices_list', imgui.ImVec2(0, 0), true) local priceHeaderY = imgui.GetCursorPosY() imgui.TextColored(khThemeColor(nil, 'accentText'), khText('Цены предметов')) khHelp('Укажи цену предметов, которые падают с кладов. По ним считается прибыль в статистике и панели Статистика.') local loadButtonW = 225 local childSize = imgui.GetWindowSize() imgui.SameLine() imgui.SetCursorPosY(priceHeaderY - 4) imgui.SetCursorPosX(math.max(250, childSize.x - loadButtonW - 30)) if khAvgPriceLoading then imgui.Button(khText('Загрузка средних...'), imgui.ImVec2(loadButtonW, 30)) elseif imgui.Button(khText('Загрузить средние цены'), imgui.ImVec2(loadButtonW, 30)) then khStartLoadAveragePrices() end imgui.SetCursorPosY(math.max(imgui.GetCursorPosY(), priceHeaderY + 34)) imgui.Separator() if #items == 0 then imgui.TextWrapped(khText('Пока нет предметов в статистике. Сначала подними клад, потом появится список для цен.')) else imgui.Columns(3, '##kh_prices_columns', false) imgui.SetColumnWidth(0, 330) imgui.SetColumnWidth(1, 150) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('Предмет')) imgui.NextColumn() imgui.TextColored(khThemeColor(nil, 'accentText'), khText('Цена за 1')) imgui.NextColumn() imgui.TextColored(khThemeColor(nil, 'accentText'), khText('Итог')) imgui.NextColumn() imgui.Separator() for index, name in ipairs(items) do local input = khGetPriceInput(name) local count = khGetDropItemTotalCount(name) local changed = false imgui.TextWrapped(khText(name)) imgui.NextColumn() imgui.PushItemWidth(-1) if imgui.InputInt ~= nil then changed = imgui.InputInt('##kh_price_' .. tostring(index), input, 100, 1000) else changed = imgui.SliderInt('##kh_price_' .. tostring(index), input, 0, 10000000) end imgui.PopItemWidth() if input[0] < 0 then input[0] = 0 end if changed then khDropPrices[name] = math.floor((tonumber(input[0]) or 0) + 0.5) khSaveDropPrices() end imgui.NextColumn() imgui.Text(khText(string.format('%s шт. / %s VC', khFormatNumber(count), khFormatNumber(count * khGetDropItemPrice(name))))) imgui.NextColumn() end imgui.Columns(1) end imgui.EndChild() end local function khRenderDopkiWindow() local sorted = khBuildSortedDopList() local selected = khAdditionalPoints[khDopSelectedIndex] local selectedDistance = nil for _, entry in ipairs(sorted) do if entry.index == khDopSelectedIndex then selectedDistance = entry.distance break end end imgui.BeginChild('##kh_dop_list', imgui.ImVec2(235, 0), true) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xd1\xef\xe8\xf1\xee\xea')) imgui.TextWrapped(khText('\xc1\xeb\xe8\xe6\xe0\xe9\xf8\xe8\xe5\x20\xf1\xe2\xe5\xf0\xf5\xf3\x2e\x20\xc2\xfb\xe1\xe5\xf0\xe8\x20\xe4\xee\xef\xea\xf3\x20\xe8\x20\xef\xee\xf1\xf2\xe0\xe2\xfc\x20\xec\xe5\xf2\xea\xf3\x2e')) imgui.Separator() if #sorted == 0 then imgui.TextColored(khThemeColor(nil, 'muted'), khText('\xc4\xee\xef\xee\xea\x20\xed\xe5\xf2\x2e')) else for _, entry in ipairs(sorted) do local point = khAdditionalPoints[entry.index] if point then local label = string.format('#%d %.0f m %s##kh_dop_%d', point.id, entry.distance, khFormatDopAge(point), point.id) if imgui.Selectable(khText(label), khDopSelectedIndex == entry.index) then khDopSelectedIndex = entry.index end end end end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_dop_info', imgui.ImVec2(230, 0), true) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xc8\xed\xf4\xee\xf0\xec\xe0\xf6\xe8\xff')) imgui.Separator() if selected then local dist = selectedDistance or 0 imgui.Text(khText(string.format('\xc4\xe8\xf1\xf2\xe0\xed\xf6\xe8\xff\x3a\x20\x25\x2e\x30\x66\x20\xec', dist))) imgui.Text(khText('Лежит: ' .. khFormatDopAge(selected))) imgui.Separator() imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xca\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb\x3a')) imgui.Text(string.format('X: %.2f', selected.x)) imgui.Text(string.format('Y: %.2f', selected.y)) imgui.Text(string.format('Z: %.2f', selected.z)) imgui.Separator() if imgui.Button(khText('\xcf\xee\xf1\xf2\xe0\xe2\xe8\xf2\xfc\x20\xec\xe5\xf2\xea\xf3'), imgui.ImVec2(-1, 30)) then khSetDopMarker(khDopSelectedIndex) end if imgui.Button(khText('\xd3\xe4\xe0\xeb\xe8\xf2\xfc\x20\xe4\xee\xef\xea\xf3'), imgui.ImVec2(-1, 30)) then khRemoveAdditionalPoint(khDopSelectedIndex) end else imgui.TextWrapped(khText('\xc2\xfb\xe1\xe5\xf0\xe8\x20\xe4\xee\xef\xea\xf3\x20\xf1\xeb\xe5\xe2\xe0\x2e')) end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_dop_settings', imgui.ImVec2(0, 0), true) imgui.TextColored(khThemeColor(nil, 'accentText'), khText('\xcd\xe0\xf1\xf2\xf0\xee\xe9\xea\xe8')) imgui.Separator() if khToggle(khText('\xcf\xee\xea\xe0\xe7\xfb\xe2\xe0\xf2\xfc\x20\x62\x6c\x69\x70\x20\xed\xe0\x20\xea\xe0\xf0\xf2\xe5'), khDopShowBlips) then khRefreshAdditionalBlips() end imgui.Text(khText('\xc8\xea\xee\xed\xea\xe0\x20\xed\xee\xe2\xfb\xf5\x20\xe4\xee\xef\xee\xea')) imgui.PushItemWidth(-1) if imgui.SliderInt('##kh_dop_default_icon', khDopDefaultIcon, 1, 63) then for _, point in ipairs(khAdditionalPoints) do point.icon = khDopDefaultIcon[0] end khSaveAdditionalPoints() khRefreshAdditionalBlips() end imgui.PopItemWidth() imgui.Separator() if imgui.Button(khText('\xce\xf7\xe8\xf1\xf2\xe8\xf2\xfc\x20\xf1\xef\xe8\xf1\xee\xea'), imgui.ImVec2(-1, 30)) then khClearAdditionalPoints() end imgui.EndChild() end local function khRenderSettingsWindow() local theme = khThemeCurrent() local settingsNoScrollFlags = 0 if imgui.WindowFlags ~= nil then if imgui.WindowFlags.NoScrollbar ~= nil then settingsNoScrollFlags = settingsNoScrollFlags + imgui.WindowFlags.NoScrollbar end if imgui.WindowFlags.NoScrollWithMouse ~= nil then settingsNoScrollFlags = settingsNoScrollFlags + imgui.WindowFlags.NoScrollWithMouse end end imgui.BeginChild('##kh_settings_main', imgui.ImVec2(0, 0), true, settingsNoScrollFlags) local themeAvail = imgui.GetContentRegionAvail() local leftWidth = math.max(250, math.floor(themeAvail.x * 0.50)) if leftWidth > themeAvail.x - 210 then leftWidth = math.max(1, themeAvail.x - 210) end local themeBlockHeight = math.max(220, math.floor(themeAvail.y - 6)) imgui.Columns(2, '##kh_theme_columns', false) imgui.SetColumnWidth(0, leftWidth) imgui.BeginChild('##kh_theme_picker_inline', imgui.ImVec2(0, themeBlockHeight), true, settingsNoScrollFlags) imgui.TextColored(khThemeColor(theme, 'accentText'), khText('Смена цвета')) imgui.Separator() local pickerChanged = false local pickerFlags = 0 if imgui.ColorEditFlags ~= nil then if imgui.ColorEditFlags.PickerHueWheel ~= nil then pickerFlags = pickerFlags + imgui.ColorEditFlags.PickerHueWheel end if imgui.ColorEditFlags.NoSidePreview ~= nil then pickerFlags = pickerFlags + imgui.ColorEditFlags.NoSidePreview end if imgui.ColorEditFlags.NoSmallPreview ~= nil then pickerFlags = pickerFlags + imgui.ColorEditFlags.NoSmallPreview end if imgui.ColorEditFlags.NoInputs ~= nil then pickerFlags = pickerFlags + imgui.ColorEditFlags.NoInputs end end local wheelW = math.max(135, math.min(190, imgui.GetContentRegionAvail().x * 0.56)) if imgui.PushItemWidth ~= nil then imgui.PushItemWidth(wheelW) end if imgui.ColorPicker4 ~= nil then imgui.SetCursorPosX(imgui.GetCursorPosX() + math.max(0, (imgui.GetContentRegionAvail().x - wheelW) * 0.5)) pickerChanged = imgui.ColorPicker4('##kh_theme_picker_color', khThemeAccent, pickerFlags) elseif imgui.ColorEdit4 ~= nil then pickerChanged = imgui.ColorEdit4('##kh_theme_picker_color', khThemeAccent) else local rr = new.int(math.floor((tonumber(khThemeAccent[0]) or 0.72) * 255 + 0.5)) local gg = new.int(math.floor((tonumber(khThemeAccent[1]) or 0.45) * 255 + 0.5)) local bb = new.int(math.floor((tonumber(khThemeAccent[2]) or 0.96) * 255 + 0.5)) pickerChanged = imgui.SliderInt('R##kh_theme_r', rr, 0, 255) or pickerChanged pickerChanged = imgui.SliderInt('G##kh_theme_g', gg, 0, 255) or pickerChanged pickerChanged = imgui.SliderInt('B##kh_theme_b', bb, 0, 255) or pickerChanged if pickerChanged then khThemeSetAccentRGB(rr[0] / 255, gg[0] / 255, bb[0] / 255) end end if imgui.PopItemWidth ~= nil then imgui.PopItemWidth() end if pickerChanged then khThemeSetAccentRGB(khThemeAccent[0], khThemeAccent[1], khThemeAccent[2]) khSaveMainSettings() theme = khThemeCurrent() end imgui.Dummy(imgui.ImVec2(0, 4)) imgui.Separator() imgui.TextColored(khThemeColor(theme, 'accentText'), khText('Быстрые цвета')) local draw = imgui.GetWindowDrawList() local currentR, currentG, currentB = khThemeCurrentRGB() local cols = 2 local gap = 6 local avail = imgui.GetContentRegionAvail() local buttonW = math.min(150, math.max(1, math.floor((avail.x - gap * (cols - 1)) / cols))) for i, preset in ipairs(khThemeSwatches) do if (i - 1) % cols ~= 0 then imgui.SameLine(0, gap) end local pr, pg, pb = preset.color[1], preset.color[2], preset.color[3] imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(math.min(1, pr * 0.40 + 0.05), math.min(1, pg * 0.40 + 0.05), math.min(1, pb * 0.40 + 0.05), 1.00)) imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(math.min(1, pr * 0.55 + 0.06), math.min(1, pg * 0.55 + 0.06), math.min(1, pb * 0.55 + 0.06), 1.00)) imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(math.min(1, pr * 0.70 + 0.06), math.min(1, pg * 0.70 + 0.06), math.min(1, pb * 0.70 + 0.06), 1.00)) if imgui.Button(khText(preset.name) .. '##kh_theme_preset_' .. tostring(i), imgui.ImVec2(buttonW, 24)) then khThemeSetAccentRGB(pr, pg, pb) khSaveMainSettings() nakhodkaNotify('Цвет меню изменён.', -1, 'success', 2) end if math.abs(currentR - pr) < 0.02 and math.abs(currentG - pg) < 0.02 and math.abs(currentB - pb) < 0.02 then if draw ~= nil and draw.AddRect ~= nil and imgui.GetItemRectMin ~= nil and imgui.GetItemRectMax ~= nil then draw:AddRect(imgui.GetItemRectMin(), imgui.GetItemRectMax(), khThemeColorU32(theme, 'accentText', 0.95), 7, 0, 2) end end imgui.PopStyleColor(3) if i % cols == 0 then imgui.Dummy(imgui.ImVec2(0, 4)) end end local rainbowR, rainbowG, rainbowB = khThemeRainbowRGB(os.clock()) local rainbowActive = khThemeRainbowMode and true or false imgui.SameLine(0, gap) imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(rainbowR * 0.40 + 0.08, rainbowG * 0.40 + 0.08, rainbowB * 0.40 + 0.08, 1.00)) imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(rainbowR * 0.58 + 0.10, rainbowG * 0.58 + 0.10, rainbowB * 0.58 + 0.10, 1.00)) imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(rainbowR * 0.72 + 0.12, rainbowG * 0.72 + 0.12, rainbowB * 0.72 + 0.12, 1.00)) if imgui.Button(khText(rainbowActive and 'Радуга вкл.' or 'Радуга') .. '##kh_theme_rainbow', imgui.ImVec2(buttonW, 24)) then khThemeSetRainbowMode(not rainbowActive) khSaveMainSettings() nakhodkaNotify(rainbowActive and 'Радужный стиль выключен.' or 'Радужный стиль включён.', -1, 'success', 2) end imgui.PopStyleColor(3) imgui.EndChild() imgui.NextColumn() imgui.BeginChild('##kh_theme_quick_inline', imgui.ImVec2(0, themeBlockHeight), true, settingsNoScrollFlags) imgui.TextColored(khThemeColor(theme, 'accentText'), khText('Эффекты')) imgui.Separator() if khBlur ~= nil then if khToggle(khText('Размытие фона'), khMenuBlurEnabled) then khSaveMainSettings() end imgui.Text(khText('Сила')) imgui.PushItemWidth(-1) if imgui.PushStyleVarFloat ~= nil and imgui.StyleVar ~= nil and imgui.StyleVar.GrabRounding ~= nil then imgui.PushStyleVarFloat(imgui.StyleVar.GrabRounding, 10) end if imgui.SliderInt('##kh_menu_blur_radius', khMenuBlurRadius, 1, 20, '') then khSaveMainSettings() end if imgui.PopStyleVar ~= nil and imgui.StyleVar ~= nil and imgui.StyleVar.GrabRounding ~= nil then imgui.PopStyleVar() end imgui.PopItemWidth() end if khParticlesLib ~= nil then if khToggle(khText('Летающие частицы'), khParticlesEnabled) then khSaveMainSettings() end end if imgui.SetWindowFontScale ~= nil then imgui.SetWindowFontScale(0.82) end imgui.PushStyleColor(imgui.Col.Text, khThemeColor(theme, 'muted')) imgui.TextWrapped(khText('(!) Может нестабильно работать с Vulkan-рендером.')) imgui.PopStyleColor() if imgui.SetWindowFontScale ~= nil then imgui.SetWindowFontScale(1.0) end imgui.Dummy(imgui.ImVec2(0, 4)) imgui.Separator() imgui.TextColored(khThemeColor(theme, 'accentText'), khText('Файлы')) imgui.Separator() if khToggle(khText('Автообновление'), khAutoUpdateEnabled) then khSaveMainSettings() end khHelp('При запуске проверяет Nakhodka Legal на сайте и ставит свежий Lua, если мы залили фикс.') if imgui.Button(khText(khUpdateChecking and 'Проверяю...' or 'Проверить обновления'), imgui.ImVec2(-1, 28)) then khCheckForScriptUpdate(true) end imgui.Dummy(imgui.ImVec2(0, 3)) if imgui.Button(khText(khFilesChecking and '...' or 'Проверить файлы'), imgui.ImVec2(-1, 28)) then if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' and not khFilesChecking then khFilesChecking = true lua_thread.create(function() local ok2, dl = pcall(nkBootstrapFiles) khFilesChecking = false dl = (ok2 and tonumber(dl)) or 0 if dl > 0 then nakhodkaNotify('Файлы загружены, скрипт перезагрузится...', -1, 'success', 4) wait(900) local sr = false if type(thisScript) == 'function' then local s = thisScript(); if s ~= nil then sr = pcall(function() s:reload() end) end end if not sr and type(reloadScripts) == 'function' then reloadScripts() end else nakhodkaNotify('Все файлы на месте', -1, 'info', 3) end end) end end imgui.Dummy(imgui.ImVec2(0, 6)) if imgui.Button(khText('Сбросить настройки'), imgui.ImVec2(-1, 28)) then khThemeSetAccentRGB(0.72, 0.45, 0.96) khThemeSetRainbowMode(false) if khMenuBlurEnabled ~= nil then khMenuBlurEnabled[0] = false end if khParticlesEnabled ~= nil then khParticlesEnabled[0] = false end pcall(khResetHudPosition) khSaveMainSettings() nakhodkaNotify('Настройки сброшены', -1, 'success', 3) end imgui.EndChild() imgui.Columns(1) imgui.EndChild() end local function khRenderInfoWindow() local accent = khThemeColor(nil, 'accentText') local muted = khThemeColor(nil, 'muted') imgui.BeginChild('##kh_info_about', imgui.ImVec2(0, 0), true) if imgui.SetWindowFontScale ~= nil then imgui.SetWindowFontScale(1.22) end imgui.TextColored(accent, 'Nakhodka') if imgui.SetWindowFontScale ~= nil then imgui.SetWindowFontScale(1.0) end imgui.TextWrapped(khText('Nakhodka Legal - это легальная версия помощника: хранит основные точки и допки, показывает метки на карте, помогает вести цены, считать прибыль и смотреть статистику выкопанных кладов. В этой сборке нет 3D-маркеров, авто-проверки точек и командного VPS-модуля.')) imgui.Separator() imgui.Columns(3, '##kh_info_modules', false) imgui.SetColumnWidth(0, 270) imgui.SetColumnWidth(1, 270) imgui.TextColored(accent, khText('Карта и зоны')) imgui.TextWrapped(khText('Сохранение последней зоны, копирование/вставка координат, восстановление прошлых зон и пересечения.')) imgui.NextColumn() imgui.TextColored(accent, khText('Метки')) imgui.TextWrapped(khText('Основные точки, допки, GPS/чекпоинты и удобный список ближайших координат без 3D-отрисовки.')) imgui.NextColumn() imgui.TextColored(accent, khText('Статистика')) imgui.TextWrapped(khText('Автолог дропа, цены предметов, прибыль, счётчики за день и общий компактный HUD.')) imgui.Columns(1) imgui.Spacing() imgui.Separator() imgui.TextColored(accent, khText('Список команд')) khRenderCommands(8) imgui.EndChild() end if imguiReady then imgui.OnFrame(function() return khMenuState ~= nil and khMenuState[0] end, function() local io = imgui.GetIO() local dt = io and io.DeltaTime or 0.016 if khTargetTab ~= khActiveTab then khContentAlpha = khContentAlpha - dt * 8 if khContentAlpha <= 0 then khActiveTab = khTargetTab khContentAlpha = 0 end else khContentAlpha = math.min(1, khContentAlpha + dt * 8) end khPushStyle() imgui.SetNextWindowSize(imgui.ImVec2(1080, 640), imgui.Cond.Always or imgui.Cond.FirstUseEver) local flags = imgui.WindowFlags.NoCollapse if imgui.WindowFlags.NoResize ~= nil then flags = flags + imgui.WindowFlags.NoResize end if imgui.WindowFlags.NoTitleBar ~= nil then flags = flags + imgui.WindowFlags.NoTitleBar end if imgui.Begin('nakhodka##main', khMenuState, flags) then if khBlurSkipFrames and khBlurSkipFrames > 0 then khBlurSkipFrames = khBlurSkipFrames - 1 end if khMenuBlurEnabled ~= nil and khMenuBlurEnabled[0] and khBlur ~= nil and imgui.GetBackgroundDrawList ~= nil and (type(isGamePaused) ~= 'function' or not isGamePaused()) and (khBlurSkipFrames == nil or khBlurSkipFrames <= 0) then pcall(function() khBlur.apply(imgui.GetBackgroundDrawList(), tonumber(khMenuBlurRadius[0]) or 8) end) end if khParticlesEnabled ~= nil and khParticlesEnabled[0] and imgui.GetBackgroundDrawList ~= nil then pcall(function() local sys = khEnsureParticles() if sys ~= nil then local pr, pg, pb = khThemeCurrentRGB() sys.line_color = {pr, pg, pb, 0.45} for _, pp in ipairs(sys.particles) do pp.color[1] = pr; pp.color[2] = pg; pp.color[3] = pb end sys:update(imgui.GetMousePos()) sys:draw(imgui.GetBackgroundDrawList(), imgui.ImVec2(0, 0)) end end) end local size = imgui.GetWindowSize() local headerY = imgui.GetCursorPosY() imgui.SetCursorPosX(math.max(14, size.x - 40)) imgui.SetCursorPosY(headerY - 2) if imgui.Button('X##kh_close', imgui.ImVec2(26, 22)) then khMenuState[0] = false end imgui.BeginChild('##kh_sidebar', imgui.ImVec2(170, 0), true) imgui.Dummy(imgui.ImVec2(0, 8)) local sidebarTitle = 'Nakhodka' if imgui.SetWindowFontScale ~= nil then imgui.SetWindowFontScale(1.85) end local titleX = imgui.GetCursorPosX() local titleWidth = imgui.CalcTextSize(sidebarTitle).x imgui.SetCursorPosX(titleX + math.max(0, (imgui.GetContentRegionAvail().x - titleWidth) * 0.5)) imgui.TextColored(khThemeColor(nil, 'accentText'), sidebarTitle) if imgui.SetWindowFontScale ~= nil then imgui.SetWindowFontScale(1.0) end imgui.Dummy(imgui.ImVec2(0, 2)) imgui.Separator() imgui.Dummy(imgui.ImVec2(0, 4)) for i = 1, #khTabs do khRenderTabButton(i) end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_content', imgui.ImVec2(0, 0), true) local tab = khTabs[khActiveTab] local view = tab and tab.view or 'main' imgui.PushStyleVarFloat(imgui.StyleVar.Alpha, khContentAlpha) imgui.TextColored(khThemeColor(nil, 'accentText'), khText(tab.title)) imgui.TextWrapped(khText(tab.desc)) imgui.Separator() if view == 'main' then khRenderMainSettingsWindow() elseif view == 'points' then khRenderMainPointsWindow() elseif view == 'stats' then khRenderDropStatsWindow() elseif view == 'prices' then khRenderPricesWindow() elseif view == 'dopki' then khRenderDopkiWindow() elseif view == 'settings' then khRenderSettingsWindow() elseif view == 'info' then khRenderInfoWindow() else khRenderEmptySection() end imgui.PopStyleVar() imgui.EndChild() end imgui.End() khPopStyle() end) end local zoneActive = false local mapUsed = true local kdcond = false local endkd = '\xcd\xe5\xe8\xe7\xe2\xe5\xf1\xf2\xed\xee.' local gangZones = {} -- \xcf\xe5\xf0\xe5\xec\xe5\xed\xed\xfb\xe5 \xe4\xeb\xff \xf5\xf0\xe0\xed\xe5\xed\xe8\xff \xe4\xe0\xed\xed\xfb\xf5 \xe0\xea\xf2\xe8\xe2\xed\xee\xe3\xee \xf7\xe5\xea\xef\xee\xe8\xed\xf2\xe0 activeMarkerCoord = nil activeMarkerRadius = 3.0 -- \xd0\xe0\xe4\xe8\xf3\xf1 \xf7\xe5\xea\xef\xee\xe8\xed\xf2\xe0 \xe2 \xec\xe5\xf2\xf0\xe0\xf5 activeCheckpointHandle = nil -- \xd5\xfd\xed\xe4\xeb \xf1\xe0\xec\xee\xe3\xee 3D-\xf6\xe8\xeb\xe8\xed\xe4\xf0\xe0 khClearMainTreasureBlips = function() for _, blip in pairs(khMainTreasureBlips) do if khBlipExists(blip) then khForgetBlip(blip) khRemoveBlip(blip) end end khMainTreasureBlips = {} end local function khIsMainPointInActiveZone(point) if not zoneActive then return false end local x = tonumber(point[1]) or 0 local y = tonumber(point[2]) or 0 local l = tonumber(left) or 0 local r = tonumber(right) or 0 local u = tonumber(up) or 0 local d = tonumber(down) or 0 local minX, maxX = math.min(l, r), math.max(l, r) local minY, maxY = math.min(u, d), math.max(u, d) return x >= minX and x <= maxX and y >= minY and y <= maxY end function khGetActiveZonePointProgress() if not zoneActive then return 0, 0 end local checked = 0 local total = 0 for index, point in ipairs(khMainTreasurePoints) do if khIsMainPointInActiveZone(point) then total = total + 1 if khMainTreasureChecked[index] then checked = checked + 1 end end end return checked, total end local function khResetMainTreasureChecked() khMainTreasureChecked = {} end local function khShouldShowMainTreasurePoint(index, point, px, py, pz) if not khIsScriptEnabled() or khIsNoTreasureServer() then return false end if khOnlyInZone[0] and not khIsMainPointInActiveZone(point) then return false end if not px or not py then return false end local dx = px - (tonumber(point[1]) or 0) local dy = py - (tonumber(point[2]) or 0) local radius = tonumber(khPointDisplayRadius[0]) or 300 return (dx * dx + dy * dy) <= radius * radius end function khUpdateMainTreasureCheckedFast(px, py) return false end khRefreshMainTreasureBlips = function(force) if force then khClearMainTreasureBlips() end if not khIsScriptEnabled() or khIsNoTreasureServer() then khClearMainTreasureBlips() khClearMainTreasureMarker() khClear3DPickups() return end local px, py, pz = getCharCoordinates(PLAYER_PED) local wanted = {} local icon = tonumber(khPointIcon[0]) or 56 for index, point in ipairs(khMainTreasurePoints) do if khShouldShowMainTreasurePoint(index, point, px, py, pz) then wanted[index] = true if khMainTreasureBlips[index] == nil or not khBlipExists(khMainTreasureBlips[index]) then local blip = khAddSpriteBlipCompat(point[1], point[2], point[3], icon) if blip then khMainTreasureBlips[index] = blip khRememberBlip(blip) end end end end for index, blip in pairs(khMainTreasureBlips) do if not wanted[index] then if khBlipExists(blip) then khForgetBlip(blip) khRemoveBlip(blip) end khMainTreasureBlips[index] = nil end end end local function khArgb(a, r, g, b) local function clamp(value, default) value = math.floor((tonumber(value) or default) + 0.5) if value < 0 then value = 0 end if value > 255 then value = 255 end return value end a = clamp(a, 255) r = clamp(r, 255) g = clamp(g, 255) b = clamp(b, 255) return a * 16777216 + r * 65536 + g * 256 + b end local function khHsvToRgb(h, s, v) local i = math.floor(h * 6) local f = h * 6 - i local p = v * (1 - s) local q = v * (1 - f * s) local t = v * (1 - (1 - f) * s) i = i % 6 if i == 0 then return v, t, p end if i == 1 then return q, v, p end if i == 2 then return p, v, t end if i == 3 then return p, q, v end if i == 4 then return t, p, v end return v, p, q end local function khRainbowColor(seed) local r, g, b = khHsvToRgb((seed or 0) % 1, 0.85, 1.0) return khArgb(230, math.floor(r * 255), math.floor(g * 255), math.floor(b * 255)) end function khIsFiniteNumber(value) return type(value) == 'number' and value == value and value > -1000000 and value < 1000000 end function khWorldPointOnScreen(x, y, z) return false end function khWorldToScreen(x, y, z) return nil, nil end function khDrawWorldCircle(x, y, z, radius, color, width) end function khClear3DPickups(maxPerFrame) return true end function khRender3DWorldMarkers() end function kh3DRequestMarkerType(value) return false end function kh3DApplyPendingMarkerType(nowClock) return false end mainCfg = inicfg.load({ LastZone = { saved = false, left = 0, up = 0, right = 0, down = 0 }, Settings = { enabled = true, onlyInZone = false, pointDisplayRadius = 300, pointCheckRadius = 12, pointIcon = 56, autoUpdate = true }, Hud = { enabled = true, x = 22, y = 220 }, Ui = { accentR = 0.72, accentG = 0.45, accentB = 0.96, rainbow = false }, Dopki = { migrated = false, count = 0 }, ThreeDMarkers = { enabled = false, antiWh = false, markerType = 0, distance = 100, arrowDistance = 300, radius10 = 15, radiusMeters = 1.5, normalR = 255, normalG = 255, normalB = 255, normalA = 255, checkedR = 255, checkedG = 51, checkedB = 51, checkedA = 255 }, Team = { enabled = false, host = '138.124.127.115', port = 27815, token = '', manualServer = '' }, Tyan = { enabled = false, x = 240, y = 500, size = 136, variant = 0 } }, config_file) if mainCfg.Settings == nil then mainCfg.Settings = { enabled = true, onlyInZone = false, pointDisplayRadius = 300, pointCheckRadius = 12, pointIcon = 56, autoUpdate = true } end if mainCfg.Hud == nil then mainCfg.Hud = {enabled = true, x = 22, y = 220} end if mainCfg.Ui == nil then mainCfg.Ui = {accentR = 0.72, accentG = 0.45, accentB = 0.96, rainbow = false} end if mainCfg.Dopki == nil then mainCfg.Dopki = {migrated = false, count = 0} end if mainCfg.ThreeDMarkers == nil then mainCfg.ThreeDMarkers = { enabled = false, antiWh = false, markerType = 0, distance = 100, arrowDistance = 300, radius10 = 15, radiusMeters = 1.5, normalR = 255, normalG = 255, normalB = 255, normalA = 255, checkedR = 255, checkedG = 51, checkedB = 51, checkedA = 255 } end if mainCfg.Team == nil then mainCfg.Team = { enabled = false, host = '138.124.127.115', port = 27815, token = '', manualServer = '' } end if mainCfg.Tyan == nil then mainCfg.Tyan = { enabled = false, x = 240, y = 500, size = 136, variant = 0 } end if mainCfg.Settings.mainPointIconDefaultMigrated == nil then local oldIcon = tonumber(mainCfg.Settings.pointIcon) if oldIcon == nil or oldIcon == 14 then mainCfg.Settings.pointIcon = 56 end mainCfg.Settings.mainPointIconDefaultMigrated = true inicfg.save(mainCfg, config_file) end local function khCfgBool(value, default) if value == nil then return default end if value == true or value == 1 then return true end local text = tostring(value):lower() if text == 'true' or text == '1' then return true elseif text == 'false' or text == '0' then return false end return default end local function khClampInt(value, default, minValue, maxValue) value = tonumber(value) or default value = math.floor(value + 0.5) if value < minValue then value = minValue end if value > maxValue then value = maxValue end return value end khScriptEnabled[0] = khCfgBool(mainCfg.Settings.enabled, true) khOnlyInZone[0] = khCfgBool(mainCfg.Settings.onlyInZone, false) khZoneOpsEnabled[0] = khCfgBool(mainCfg.Settings.zoneOps, true) khPointDisplayRadius[0] = khClampInt(mainCfg.Settings.pointDisplayRadius, 300, 50, 2000) khPointCheckRadius[0] = khClampInt(mainCfg.Settings.pointCheckRadius, 12, 1, 100) khPointIcon[0] = khClampInt(mainCfg.Settings.pointIcon, 56, 1, 63) khAutoUpdateEnabled[0] = khCfgBool(mainCfg.Settings.autoUpdate, true) khDropHudEnabled[0] = khCfgBool(mainCfg.Hud.enabled, true) khHudPos.x = tonumber(mainCfg.Hud.x) or 22 khHudPos.y = tonumber(mainCfg.Hud.y) or 220 kh3DCfg = mainCfg.ThreeDMarkers or {} kh3DMarkersEnabled[0] = false kh3DMarkersAntiWh[0] = false kh3DMarkerType[0] = khClampInt(kh3DCfg.markerType, 0, 0, 1) kh3DMarkerDistance[0] = khClampInt(kh3DCfg.distance, 100, 30, 300) kh3DArrowDistance[0] = 30 local radiusMeters = tonumber(kh3DCfg.radiusMeters) if radiusMeters == nil then radiusMeters = (tonumber(kh3DCfg.radius10) or 15) / 10 end kh3DMarkerRadius10[0] = khClampFloat(radiusMeters, 1.5, 0.5, 5.0) kh3DNormalR[0] = khClampInt(kh3DCfg.normalR, 255, 0, 255) kh3DNormalG[0] = khClampInt(kh3DCfg.normalG, 255, 0, 255) kh3DNormalB[0] = khClampInt(kh3DCfg.normalB, 255, 0, 255) kh3DNormalA[0] = khClampInt(kh3DCfg.normalA, 255, 0, 255) kh3DCheckedR[0] = khClampInt(kh3DCfg.checkedR, 255, 0, 255) kh3DCheckedG[0] = khClampInt(kh3DCfg.checkedG, 51, 0, 255) kh3DCheckedB[0] = khClampInt(kh3DCfg.checkedB, 51, 0, 255) kh3DCheckedA[0] = khClampInt(kh3DCfg.checkedA, 255, 0, 255) khTeamCfg = mainCfg.Team or {} khTeamEnabled[0] = false khTeam.host = tostring(khTeamCfg.host or '138.124.127.115') khTeam.port = khClampInt(khTeamCfg.port, 27815, 1, 65535) khTeamPort[0] = khTeam.port khTeam.token = tostring(khTeamCfg.token or '') khTeam.manualServer = tostring(khTeamCfg.manualServer or '') khTyanCfg = mainCfg.Tyan or {} khTyanEnabled[0] = khCfgBool(khTyanCfg.enabled, false) khTyanPos.x = tonumber(khTyanCfg.x) or 240 khTyanPos.y = tonumber(khTyanCfg.y) or 500 khTyanSize = khClampInt(khTyanCfg.size, 260, 80, 400) khTyanSizeInput[0] = khTyanSize khTyanVariant = khClampInt(khTyanCfg.variant, 0, 0, 1) khThemeRainbowMode = khCfgBool(mainCfg.Ui.rainbow, false) khMenuBlurEnabled[0] = khCfgBool(mainCfg.Ui.blur, false) khMenuBlurRadius[0] = khClampInt(mainCfg.Ui.blurRadius, 8, 1, 20) khParticlesEnabled[0] = khCfgBool(mainCfg.Ui.particles, false) khThemeApplyAccentRGB(mainCfg.Ui.accentR, mainCfg.Ui.accentG, mainCfg.Ui.accentB) khSaveMainSettings = function() if mainCfg.Settings == nil then mainCfg.Settings = {} end if mainCfg.Hud == nil then mainCfg.Hud = {} end if mainCfg.Ui == nil then mainCfg.Ui = {} end if mainCfg.ThreeDMarkers == nil then mainCfg.ThreeDMarkers = {} end if mainCfg.Team == nil then mainCfg.Team = {} end if mainCfg.Tyan == nil then mainCfg.Tyan = {} end mainCfg.Settings.enabled = khScriptEnabled[0] mainCfg.Settings.onlyInZone = khOnlyInZone[0] mainCfg.Settings.zoneOps = khZoneOpsEnabled[0] mainCfg.Settings.pointDisplayRadius = khPointDisplayRadius[0] mainCfg.Settings.pointCheckRadius = khPointCheckRadius[0] mainCfg.Settings.pointIcon = khPointIcon[0] mainCfg.Settings.autoUpdate = khAutoUpdateEnabled[0] and true or false mainCfg.Hud.enabled = khDropHudEnabled[0] mainCfg.Hud.x = math.floor((tonumber(khHudPos.x) or 22) + 0.5) mainCfg.Hud.y = math.floor((tonumber(khHudPos.y) or 220) + 0.5) mainCfg.Ui.rainbow = khThemeRainbowMode and true or false mainCfg.Ui.blur = khMenuBlurEnabled[0] and true or false mainCfg.Ui.blurRadius = tonumber(khMenuBlurRadius[0]) or 8 mainCfg.Ui.particles = khParticlesEnabled[0] and true or false if not khThemeRainbowMode then mainCfg.Ui.accentR = tonumber(khThemeAccent and khThemeAccent[0]) or 0.72 mainCfg.Ui.accentG = tonumber(khThemeAccent and khThemeAccent[1]) or 0.45 mainCfg.Ui.accentB = tonumber(khThemeAccent and khThemeAccent[2]) or 0.96 end mainCfg.ThreeDMarkers.enabled = false mainCfg.ThreeDMarkers.antiWh = false mainCfg.ThreeDMarkers.markerType = kh3DMarkerType[0] mainCfg.ThreeDMarkers.distance = kh3DMarkerDistance[0] mainCfg.ThreeDMarkers.arrowDistance = 30 local radiusMeters = khClampFloat(kh3DMarkerRadius10[0], 1.5, 0.5, 5.0) kh3DMarkerRadius10[0] = radiusMeters mainCfg.ThreeDMarkers.radiusMeters = radiusMeters mainCfg.ThreeDMarkers.radius10 = math.floor(radiusMeters * 10 + 0.5) mainCfg.ThreeDMarkers.normalR = kh3DNormalR[0] mainCfg.ThreeDMarkers.normalG = kh3DNormalG[0] mainCfg.ThreeDMarkers.normalB = kh3DNormalB[0] mainCfg.ThreeDMarkers.normalA = kh3DNormalA[0] mainCfg.ThreeDMarkers.checkedR = kh3DCheckedR[0] mainCfg.ThreeDMarkers.checkedG = kh3DCheckedG[0] mainCfg.ThreeDMarkers.checkedB = kh3DCheckedB[0] mainCfg.ThreeDMarkers.checkedA = kh3DCheckedA[0] mainCfg.Team.enabled = false mainCfg.Team.host = khTeam.host mainCfg.Team.port = khTeamPort[0] mainCfg.Team.token = khTeam.token mainCfg.Team.manualServer = khTeam.manualServer mainCfg.Tyan.enabled = khTyanEnabled[0] mainCfg.Tyan.x = math.floor((tonumber(khTyanPos.x) or 240) + 0.5) mainCfg.Tyan.y = math.floor((tonumber(khTyanPos.y) or 500) + 0.5) mainCfg.Tyan.size = math.floor((tonumber(khTyanSize) or 260) + 0.5) mainCfg.Tyan.variant = khTyanVariant == 1 and 1 or 0 inicfg.save(mainCfg, config_file) end khTeamSaveSettings = function() khTeam.port = tonumber(khTeamPort[0]) or khTeam.port or 27815 khSaveMainSettings() end local function khSaveHudPosition() if mainCfg.Hud == nil then mainCfg.Hud = {} end if mainCfg.Ui == nil then mainCfg.Ui = {} end if mainCfg.ThreeDMarkers == nil then mainCfg.ThreeDMarkers = {} end if mainCfg.Team == nil then mainCfg.Team = {} end if mainCfg.Tyan == nil then mainCfg.Tyan = {} end mainCfg.Hud.enabled = khDropHudEnabled[0] mainCfg.Hud.x = math.floor((tonumber(khHudPos.x) or 22) + 0.5) mainCfg.Hud.y = math.floor((tonumber(khHudPos.y) or 220) + 0.5) mainCfg.Ui.rainbow = khThemeRainbowMode and true or false if not khThemeRainbowMode then mainCfg.Ui.accentR = tonumber(khThemeAccent and khThemeAccent[0]) or 0.72 mainCfg.Ui.accentG = tonumber(khThemeAccent and khThemeAccent[1]) or 0.45 mainCfg.Ui.accentB = tonumber(khThemeAccent and khThemeAccent[2]) or 0.96 end mainCfg.ThreeDMarkers.enabled = false mainCfg.ThreeDMarkers.antiWh = false mainCfg.ThreeDMarkers.markerType = kh3DMarkerType[0] mainCfg.ThreeDMarkers.distance = kh3DMarkerDistance[0] mainCfg.ThreeDMarkers.arrowDistance = 30 local radiusMeters = khClampFloat(kh3DMarkerRadius10[0], 1.5, 0.5, 5.0) kh3DMarkerRadius10[0] = radiusMeters mainCfg.ThreeDMarkers.radiusMeters = radiusMeters mainCfg.ThreeDMarkers.radius10 = math.floor(radiusMeters * 10 + 0.5) mainCfg.ThreeDMarkers.normalR = kh3DNormalR[0] mainCfg.ThreeDMarkers.normalG = kh3DNormalG[0] mainCfg.ThreeDMarkers.normalB = kh3DNormalB[0] mainCfg.ThreeDMarkers.normalA = kh3DNormalA[0] mainCfg.ThreeDMarkers.checkedR = kh3DCheckedR[0] mainCfg.ThreeDMarkers.checkedG = kh3DCheckedG[0] mainCfg.ThreeDMarkers.checkedB = kh3DCheckedB[0] mainCfg.ThreeDMarkers.checkedA = kh3DCheckedA[0] mainCfg.Team.enabled = false mainCfg.Team.host = khTeam.host mainCfg.Team.port = khTeamPort[0] mainCfg.Team.token = khTeam.token mainCfg.Team.manualServer = khTeam.manualServer mainCfg.Tyan.enabled = khTyanEnabled[0] mainCfg.Tyan.x = math.floor((tonumber(khTyanPos.x) or 240) + 0.5) mainCfg.Tyan.y = math.floor((tonumber(khTyanPos.y) or 500) + 0.5) mainCfg.Tyan.size = math.floor((tonumber(khTyanSize) or 260) + 0.5) mainCfg.Tyan.variant = khTyanVariant == 1 and 1 or 0 inicfg.save(mainCfg, config_file) end khResetHudPosition = function() khHudPos.x = 22 khHudPos.y = 220 khSaveHudPosition() end khReloadMainTreasureState = function() khMainPointListCache = {} khMainPointListCacheAt = 0 khMainTreasureLastRefresh = 0 kh3DMainDrawList = {} kh3DDopDrawList = {} kh3DLastBuildAt = 0 kh3DActiveMarkerType = nil kh3DSwitchCooldownUntil = os.clock() + 0.45 kh3DForceClear = true kh3DSwitchPending = true pcall(khResetMainTreasureChecked) pcall(khClearMainTreasureMarker) pcall(khClear3DPickups) pcall(khClearMainTreasureBlips) pcall(khRefreshMainTreasureBlips, true) nakhodkaNotify(sName .. 'Точки и 3D-маркеры перезагружены.', -1, 'success', 2) end khClearMainTreasureMarker = function() if activeMarkerCoord ~= nil and type(removeWaypoint) == 'function' then pcall(removeWaypoint) end if activeCheckpointHandle then pcall(deleteCheckpoint, activeCheckpointHandle) activeCheckpointHandle = nil end activeMarkerCoord = nil khActiveMainPointIndex = nil end local function khSetMainTreasureMarker(index) if khIsNoTreasureServer() then khClearMainTreasureMarker() khClearMainTreasureBlips() nakhodkaNotify('На Vice City кладов нет, метка не ставится.', -1, 'info', 3) return false end local point = khMainTreasurePoints[index] if not point then return false end local x = tonumber(point[1]) local y = tonumber(point[2]) local z = tonumber(point[3]) or 0 if not x or not y then return false end khClearMainTreasureMarker() if type(placeWaypoint) == 'function' then pcall(placeWaypoint, x, y, z) end if type(createCheckpoint) == 'function' then local ok, checkpoint = pcall(createCheckpoint, 1, x, y, z, 0.0, 0.0, 0.0, activeMarkerRadius) if ok then activeCheckpointHandle = checkpoint end end activeMarkerCoord = {x = x, y = y, z = z} khActiveMainPointIndex = index nakhodkaNotify(sName .. string.format('Метка на точку #%d поставлена.', index), -1, 'success', 2) return true end khToggleMainPointMarker = function(index) local now = os.clock() if khMainPointMarkerBusy or now < khMainPointMarkerCooldownUntil then return false end khMainPointMarkerBusy = true khMainPointMarkerCooldownUntil = now + 0.18 local ok, result = pcall(function() if khActiveMainPointIndex == index and activeMarkerCoord ~= nil then khClear3DPickups() khClearMainTreasureMarker() nakhodkaNotify(sName .. 'Метка на точку удалена.', -1, 'info', 2) return false end return khSetMainTreasureMarker(index) end) khMainPointMarkerBusy = false if not ok then khMainPointMarkerCooldownUntil = os.clock() + 0.35 return false end return result end local function khSaveZoneState(saved) if mainCfg.LastZone == nil then mainCfg.LastZone = {} end mainCfg.LastZone.left = left mainCfg.LastZone.up = up mainCfg.LastZone.right = right mainCfg.LastZone.down = down mainCfg.LastZone.saved = saved and true or false inicfg.save(mainCfg, config_file) end local function khMarkZoneInactive() if type(removeGangZone) == 'function' then removeGangZone(610) end zoneActive = false mapUsed = false khClearMainTreasureMarker() khSaveZoneState(false) khRefreshMainTreasureBlips(true) khTeamQueueZoneNow() end local function khMarkZoneActive() zoneActive = true mapUsed = true khResetMainTreasureChecked() khSaveZoneState(true) khRefreshMainTreasureBlips(true) khTeamQueueZoneNow() end khApplyScriptEnabledState = function() khHudMoveMode = false khHudSaveLatch = false if khIsScriptEnabled() then if mainCfg.LastZone and mainCfg.LastZone.saved and type(addGangZone) == 'function' then left = mainCfg.LastZone.left up = mainCfg.LastZone.up right = mainCfg.LastZone.right down = mainCfg.LastZone.down removeGangZone(610) addGangZone(610, left, up, right, down, -2130706433) zoneActive = true end khRefreshAdditionalBlips() khRefreshMainTreasureBlips(true) nakhodkaNotify('Nakhodka включена.', -1, 'success', 2) else khClearActiveDopMarker() khRefreshAdditionalBlips() khClearMainTreasureBlips() khClear3DPickups() khClearMainTreasureMarker() if type(removeGangZone) == 'function' then removeGangZone(610) end zoneActive = false mapUsed = false kladZone = nil kdcond = false endkd = '\xcd\xe5\xe8\xe7\xe2\xe5\xf1\xf2\xed\xee.' nakhodkaNotify('Nakhodka выключена.', -1, 'info', 2) end end if imguiReady then imgui.OnFrame(function() return khDropHudReady == true and khIsScriptEnabled() and khDropHudEnabled ~= nil and khDropHudEnabled[0] end, function() local flags = 0 if imgui.WindowFlags.NoTitleBar ~= nil then flags = flags + imgui.WindowFlags.NoTitleBar end if imgui.WindowFlags.NoResize ~= nil then flags = flags + imgui.WindowFlags.NoResize end if imgui.WindowFlags.AlwaysAutoResize ~= nil then flags = flags + imgui.WindowFlags.AlwaysAutoResize end if imgui.WindowFlags.NoCollapse ~= nil then flags = flags + imgui.WindowFlags.NoCollapse end if imgui.WindowFlags.NoMove ~= nil then flags = flags + imgui.WindowFlags.NoMove end if imgui.WindowFlags.NoSavedSettings ~= nil then flags = flags + imgui.WindowFlags.NoSavedSettings end local io = imgui.GetIO() if khHudMoveMode and io and io.MousePos then khHudPos.x = math.max(0, io.MousePos.x - 18) khHudPos.y = math.max(0, io.MousePos.y - 12) end imgui.SetNextWindowPos(imgui.ImVec2(khHudPos.x, khHudPos.y), imgui.Cond.Always) imgui.PushStyleVarFloat(imgui.StyleVar.WindowRounding, 10) imgui.PushStyleVarFloat(imgui.StyleVar.FrameRounding, 8) imgui.PushStyleVarVec2(imgui.StyleVar.WindowPadding, imgui.ImVec2(8, 7)) imgui.PushStyleColor(imgui.Col.WindowBg, khThemeColor(nil, 'window', 0.90)) imgui.PushStyleColor(imgui.Col.Text, khThemeColor(nil, 'text')) imgui.PushStyleColor(imgui.Col.Button, khThemeColor(nil, 'button')) imgui.PushStyleColor(imgui.Col.ButtonHovered, khThemeColor(nil, 'buttonHovered')) imgui.PushStyleColor(imgui.Col.ButtonActive, khThemeColor(nil, 'buttonActive')) if imgui.Begin('kh_drop_hud##main', khDropHudEnabled, flags) then local draw = imgui.GetWindowDrawList() local white = imgui.ImVec4(0.96, 0.94, 1.00, 1.00) local muted = imgui.ImVec4(0.56, 0.55, 0.60, 1.00) local green = imgui.ImVec4(0.17, 0.78, 0.34, 1.00) local blue = imgui.ImVec4(0.28, 0.50, 0.95, 1.00) local gold = imgui.ImVec4(0.96, 0.76, 0.18, 1.00) local zoneRed = imgui.ImVec4(0.90, 0.28, 0.34, 1.00) local function toU32(color) return imgui.ColorConvertFloat4ToU32(color) end local function strongText(text, color) imgui.TextColored(color or white, khText(text)) end local function drawHudIcon(kind, p, color) local icon = khBiIcons.profit if kind == 'today' then icon = khBiIcons.today elseif kind == 'total' then icon = khBiIcons.total elseif kind == 'map' then icon = zoneActive and khBiIcons.mapMarked or khBiIcons.map end draw:AddText(imgui.ImVec2(p.x + 1, p.y + 2), toU32(color), icon) end local moveButtonSize = imgui.ImVec2(27, 20) local moveButtonPos = imgui.GetCursorScreenPos() local moveLineY = imgui.GetCursorPosY() local moveButtonActive = (not khHudMoveMode) and os.clock() >= khHudMoveCooldown local movePressed = false if imgui.InvisibleButton ~= nil then movePressed = imgui.InvisibleButton('##kh_hud_move', moveButtonSize) else movePressed = imgui.Button('##kh_hud_move', moveButtonSize) end local moveBg = khThemeColor(nil, 'button') if moveButtonActive and imgui.IsItemHovered ~= nil and imgui.IsItemHovered() then moveBg = khThemeColor(nil, 'buttonHovered') elseif not moveButtonActive then moveBg = khThemeColor(nil, 'frame') end draw:AddRectFilled(moveButtonPos, imgui.ImVec2(moveButtonPos.x + moveButtonSize.x, moveButtonPos.y + moveButtonSize.y), toU32(moveBg), 5) draw:AddRect(moveButtonPos, imgui.ImVec2(moveButtonPos.x + moveButtonSize.x, moveButtonPos.y + moveButtonSize.y), khThemeColorU32(nil, 'border', 0.60), 5) draw:AddText(imgui.ImVec2(moveButtonPos.x + 6, moveButtonPos.y + 3), toU32(white), khBiIcons.move) if moveButtonActive and movePressed then khHudMoveMode = true khHudMoveArmed = false khHudSaveLatch = true end imgui.SameLine() imgui.SetCursorPosY(moveLineY + 2) strongText(khHudMoveMode and 'ЛКМ - сохранить' or 'Статистика', white) imgui.Separator() local function row(icon, iconColor, label, value, textColor) local p = imgui.GetCursorScreenPos() drawHudIcon(icon, p, iconColor) imgui.SetCursorPosX(imgui.GetCursorPosX() + 24) local line = label ~= '' and (label .. ' ' .. tostring(value)) or tostring(value) strongText(line, textColor or white) end local zoneDone, zoneTotal = khGetActiveZonePointProgress() local zoneText = zoneActive and string.format('Зона: %d / %d', zoneDone, zoneTotal) or 'Зона не активна' local zoneIconColor = zoneActive and zoneRed or muted local zoneTextColor = zoneActive and white or muted row('profit', green, 'Прибыль:', khFormatNumber(khGetDropProfit()) .. ' VC', white) row('today', blue, 'Сегодня:', tostring(khGetTodayDropCount()) .. ' кл.', white) row('total', gold, 'Всего:', tostring(khGetTotalDropCount()) .. ' кл.', white) row('map', zoneIconColor, '', zoneText, zoneTextColor) if khHudMoveMode then local clicked = false if imgui.IsMouseClicked ~= nil then clicked = clicked or imgui.IsMouseClicked(0) end if io and io.MouseClicked ~= nil then clicked = clicked or io.MouseClicked[0] end if type(isKeyJustPressed) == 'function' then clicked = clicked or isKeyJustPressed(1) end if type(wasKeyPressed) == 'function' then clicked = clicked or wasKeyPressed(1) end local down = false if io and io.MouseDown ~= nil then down = down or io.MouseDown[0] end if type(isKeyDown) == 'function' then down = down or isKeyDown(1) end if khHudSaveLatch then if not down then khHudSaveLatch = false khHudMoveArmed = true end elseif khHudMoveArmed and clicked then khHudMoveMode = false khHudMoveArmed = false khHudSaveLatch = true khHudMoveCooldown = os.clock() + 0.30 khSaveHudPosition() nakhodkaNotify('Позиция Статистики сохранена.', -1, 'success', 2) elseif not down and not clicked then khHudMoveArmed = true end end end imgui.End() imgui.PopStyleColor(5) imgui.PopStyleVar(3) end).HideCursor = true end if imguiReady then imgui.OnFrame(function() return khDropHudReady == true and khIsScriptEnabled() and khTyanEnabled ~= nil and khTyanEnabled[0] end, function() khTyanRenderOverlay() end).HideCursor = true end -- Zone class Zone = { left = 0, up = 0, right = 0, down = 0, } left = 0 up = 0 right = 0 down = 0 -- \xcc\xe5\xf2\xee\xe4 \xe4\xeb\xff \xf1\xee\xe7\xe4\xe0\xed\xe8\xff \xed\xee\xe2\xee\xe9 \xe7\xee\xed\xfb function Zone:new(left, up, right, down) local obj = {} setmetatable(obj, self) self.__index = self obj.left = tonumber(left or 0) obj.up = tonumber(up or 0) obj.right = tonumber(right or 0) obj.down = tonumber(down or 0) return obj end -- \xcc\xe5\xf2\xee\xe4 \xe4\xeb\xff \xea\xf0\xe0\xf1\xe8\xe2\xee\xe3\xee \xe2\xfb\xe2\xee\xe4\xe0 \xe4\xe0\xed\xed\xfb\xf5 \xee \xe7\xee\xed\xe5 function Zone:str() return string.format("l: %.15f; u: %.15f; r: %.15f; d: %.15f", self.left, self.up, self.right, self.down) end -- \xd4\xf3\xed\xea\xf6\xe8\xff \xe4\xeb\xff \xed\xe0\xf5\xee\xe6\xe4\xe5\xed\xe8\xff \xec\xe8\xed\xe8\xec\xf3\xec\xe0 function my_min(a, b) if type(a) ~= "number" or type(b) ~= "number" then print("\xce\xf8\xe8\xe1\xea\xe0: \xce\xe1\xe0 \xe0\xf0\xe3\xf3\xec\xe5\xed\xf2\xe0 \xe4\xee\xeb\xe6\xed\xfb \xe1\xfb\xf2\xfc \xf7\xe8\xf1\xeb\xe0\xec\xe8.") return nil end local result = (a < b) and a or b print(string.format("\xd0\xe5\xe7\xf3\xeb\xfc\xf2\xe0\xf2 my_min: %.15f", result)) return result end -- \xd4\xf3\xed\xea\xf6\xe8\xff \xe4\xeb\xff \xed\xe0\xf5\xee\xe6\xe4\xe5\xed\xe8\xff \xec\xe0\xea\xf1\xe8\xec\xf3\xec\xe0 function my_max(a, b) if type(a) ~= "number" or type(b) ~= "number" then print("\xce\xf8\xe8\xe1\xea\xe0: \xce\xe1\xe0 \xe0\xf0\xe3\xf3\xec\xe5\xed\xf2\xe0 \xe4\xee\xeb\xe6\xed\xfb \xe1\xfb\xf2\xfc \xf7\xe8\xf1\xeb\xe0\xec\xe8.") return nil end local result = (a > b) and a or b print(string.format("\xd0\xe5\xe7\xf3\xeb\xfc\xf2\xe0\xf2 my_max: %.15f", result)) return result end -- \xcc\xe5\xf2\xee\xe4 \xe4\xeb\xff \xed\xe0\xf5\xee\xe6\xe4\xe5\xed\xe8\xff \xef\xe5\xf0\xe5\xf1\xe5\xf7\xe5\xed\xe8\xff \xe4\xe2\xf3\xf5 \xe7\xee\xed function Zone:intersect(otherZone) print("intersec zone " .. self:str() .. " and zone " .. otherZone:str()) local intersection = Zone:new( my_max(self.left, otherZone.left), my_max(self.up, otherZone.up), my_min(self.right, otherZone.right), my_min(self.down, otherZone.down) ) print("result is: " .. intersection:str()) if intersection.left <= intersection.right and intersection.up <= intersection.down then return intersection else return nil end end -- \xce\xef\xf0\xe5\xe4\xe5\xeb\xe5\xed\xe8\xe5 \xea\xeb\xe0\xf1\xf1\xe0 "ZoneList" ZoneList = { zones = {}, } function ZoneList:new() local obj = {} setmetatable(obj, self) self.__index = self obj.zones = {} return obj end function ZoneList:addZone(zone) print("add zone to zonelist: ", zone) table.insert(self.zones, zone) end function ZoneList:getZoneFromEnd(n) if n > 0 and n <= #self.zones then print("get zone " .. n .. "from end (" .. (#self.zones - n + 1) .. "/" .. #self.zones .. ")") print("zone is: ", self.zones[#self.zones - n + 1]) return self.zones[#self.zones - n + 1] else return nil end end function ZoneList:intersectLastN(n) if n == nil or n <= 0 or #self.zones < n then return nil end local result = self.zones[#self.zones] for i = #self.zones - 1, #self.zones - n + 1, -1 do if result == nil or self.zones[i] == nil then return nil end result = result:intersect(self.zones[i]) end return result end if setClipboardText == nil then function setClipboardText(text) print("need to set clipboard: " .. text) nakhodkaNotify(sName .. '\xca \xf1\xee\xe6\xe0\xeb\xe5\xed\xe8\xfe \xc2\xe0\xf8\xe0 \xf1\xe8\xf1\xf2\xe5\xec\xe0 \xed\xe5 \xef\xee\xe4\xe4\xe5\xf0\xe6\xe8\xe2\xe0\xe5\xf2 \xea\xee\xef\xe8\xf0\xee\xe2\xe0\xed\xe8\xe5 \xe2 \xe1\xf3\xf4\xe5\xf0 \xee\xe1\xec\xe5\xed\xe0. \xd2\xe5\xea\xf1\xf2 \xea\xee\xf2\xee\xf0\xfb\xe9 \xe2\xfb \xef\xfb\xf2\xe0\xeb\xe8\xf1\xfc \xf1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xf2\xfc: ', -1) nakhodkaNotify(sName .. text, -1) end end zoneList = ZoneList:new() function main() while not isSampAvailable() do wait(0) end wait(100) -- Heal missing files inside the script coroutine, where wait() actually blocks on async -- downloads (top-level bootstrap cannot wait). Auto-reload once if anything was fetched. if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() local ok, downloaded = pcall(nkBootstrapFiles) downloaded = (ok and tonumber(downloaded)) or 0 if downloaded > 0 then nakhodkaNotify('Файлы загружены, скрипт перезагрузится...', -1, 'success', 4) print('Nakhodka: files downloaded, reloading script...') wait(1500) local selfReloaded = false if type(thisScript) == 'function' then local selfScript = thisScript() if selfScript ~= nil then selfReloaded = pcall(function() selfScript:reload() end) end end if not selfReloaded and type(reloadScripts) == 'function' then reloadScripts() end return end pcall(khLoadCompanionPhrases) pcall(khLoadMainTreasurePoints) pcall(khResetMainTreasureChecked) pcall(khRefreshMainTreasureBlips, true) khTyanTextures = {} khTyanTextureTriedByVariant = {} khTyanTexture = nil khTyanTextureTried = false end) end if khAutoUpdateEnabled ~= nil and khAutoUpdateEnabled[0] then khCheckForScriptUpdate(false) end khRemoveRuntimeCleanupArtifacts() wait(0) khLoadCompanionPhrases() -- Debug seed zones removed for release builds. -- \xc7\xe0\xe3\xf0\xf3\xe7\xea\xe0 \xef\xee\xf1\xeb\xe5\xe4\xed\xe5\xe9 \xe7\xee\xed\xfb \xe8\xe7 \xea\xee\xed\xf4\xe8\xe3\xe0 \xef\xf0\xe8 \xf1\xf2\xe0\xf0\xf2\xe5 if mainCfg.LastZone.saved then zoneList:addZone(Zone:new(mainCfg.LastZone.left, mainCfg.LastZone.up, mainCfg.LastZone.right, mainCfg.LastZone.down)) end khLoadAdditionalPoints() khRemoveExpiredAdditionalPoints(false) khRefreshAdditionalBlips() khLoadDropPrices() khLoadDropStats() khPruneDropPricesToKnownItems(true) khLoadMainTreasurePoints() khResetMainTreasureChecked() khRefreshMainTreasureBlips(true) zonedel_func = function() if not khCommandEnabled() then return end if khZoneOpsEnabled ~= nil and not khZoneOpsEnabled[0] then nakhodkaNotify(sName .. 'Операции с зоной выключены.', -1, 'info') return end khMarkZoneInactive() nakhodkaNotify(sName .. 'Зона не активна. Метки основных кладов этой зоны удалены.', -1, 'info') end sampRegisterChatCommand('zonedel', zonedel_func) sampRegisterChatCommand('zd', zonedel_func) zonecopy_func = function() if not khCommandEnabled() then return end if khZoneOpsEnabled ~= nil and not khZoneOpsEnabled[0] then nakhodkaNotify(sName .. 'Операции с зоной выключены.', -1, 'info') return end if mapUsed then setClipboardText('/zonepaste l: ' .. left .. '; u: ' .. up .. '; r: ' .. right .. '; d: ' .. down) print('/zonepaste l: ' .. left .. '; u: ' .. up .. '; r: ' .. right .. '; d: ' .. down .. ' - \xf1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xed\xee') nakhodkaNotify(sName .. '\xd1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xed\xee! \xce\xf2\xef\xf0\xe0\xe2\xfc \xfd\xf2\xee \xf7\xe5\xeb\xee\xe2\xe5\xea\xf3, \xf1 \xea\xee\xf2\xee\xf0\xfb\xec \xf5\xee\xf7\xe5\xf8\xfc \xef\xee\xe4\xe5\xeb\xe8\xf2\xfc\xf1\xff \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xe0\xec\xe8.', -1) else nakhodkaNotify(sName .. '\xc0\xea\xf2\xe8\xe2\xe8\xf0\xf3\xe9 \xea\xe0\xf0\xf2\xf3 \xea\xeb\xe0\xe4\xee\xe2, \xf7\xf2\xee\xe1\xfb \xf1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xf2\xfc \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb.', -1) end end sampRegisterChatCommand('zonecopy', zonecopy_func) sampRegisterChatCommand('zc', zonecopy_func) zonepaste_func = function(coord) if not khCommandEnabled() then return end if khZoneOpsEnabled ~= nil and not khZoneOpsEnabled[0] then nakhodkaNotify(sName .. 'Операции с зоной выключены.', -1, 'info') return end zonepaste = not zonepaste if zonepaste then if #coord ~= 0 then if coord:match('l: (.*); u: (.*); r: (.*); d: (.*)') then pLeft, pUp, pRight, pDown = coord:match('l: (.*); u: (.*); r: (.*); d: (.*)') left = pLeft up = pUp right = pRight down = pDown removeGangZone(610) addGangZone(610, pLeft, pUp, pRight, pDown, -2130706433) zoneList:addZone(Zone:new(pLeft, pUp, pRight, pDown)) zoneActive = true -- \xd1\xee\xf5\xf0\xe0\xed\xff\xe5\xec \xe2\xf1\xf2\xe0\xe2\xeb\xe5\xed\xed\xf3\xfe \xe7\xee\xed\xf3 \xe2 \xea\xee\xed\xf4\xe8\xe3 mainCfg.LastZone.left = pLeft mainCfg.LastZone.up = pUp mainCfg.LastZone.right = pRight mainCfg.LastZone.down = pDown mainCfg.LastZone.saved = true inicfg.save(mainCfg, config_file) khMarkZoneActive() nakhodkaNotify(sName .. '\xd2\xe5\xf0\xf0\xe8\xf2\xee\xf0\xe8\xff \xe1\xfb\xeb\xe0 \xf3\xf1\xef\xe5\xf8\xed\xee \xe4\xee\xe1\xe0\xe2\xeb\xe5\xed\xe0 \xed\xe0 \xf2\xe2\xee\xfe \xea\xe0\xf0\xf2\xf3!', -1) else nakhodkaNotify(sName .. '\xcd\xe5 \xf2\xee\xf2 \xf4\xee\xf0\xec\xe0\xf2! \xc2\xf1\xf2\xe0\xe2\xfc \xf1\xfe\xe4\xe0 \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb, \xea\xee\xf2\xee\xf0\xfb\xe5 \xf2\xe5\xe1\xe5 \xee\xf2\xef\xf0\xe0\xe2\xe8\xeb \xe4\xf0\xf3\xe3 \xf1 \xea\xe0\xf0\xf2\xee\xe9 \xea\xeb\xe0\xe4\xee\xe2.', -1) end else nakhodkaNotify(sName .. '\xd2\xfb \xed\xe8\xf7\xe5\xe3\xee \xed\xe5 \xe2\xe2\xb8\xeb! \xc2\xf1\xf2\xe0\xe2\xfc \xf1\xfe\xe4\xe0 \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb, \xea\xee\xf2\xee\xf0\xfb\xe5 \xf2\xe5\xe1\xe5 \xee\xf2\xef\xf0\xe0\xe2\xe8\xeb \xe4\xf0\xf3\xe3 \xf1 \xea\xe0\xf0\xf2\xee\xe9 \xea\xeb\xe0\xe4\xee\xe2.', -1) end else if zoneActive then khMarkZoneInactive() nakhodkaNotify(sName .. 'Зона не активна. Метки основных кладов этой зоны удалены.', -1, 'info') else nakhodkaNotify(sName .. '\xcd\xe5 \xf2\xee\xf2 \xf4\xee\xf0\xec\xe0\xf2! \xc2\xf1\xf2\xe0\xe2\xfc \xf1\xfe\xe4\xe0 \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb, \xea\xee\xf2\xee\xf0\xfb\xe5 \xf2\xe5\xe1\xe5 \xee\xf2\xef\xf0\xe0\xe2\xe8\xeb \xe4\xf0\xf3\xe3 \xf1 \xea\xe0\xf0\xf2\xee\xe9 \xea\xeb\xe0\xe4\xee\xe2.', -1) end end end sampRegisterChatCommand('zonepaste', zonepaste_func) sampRegisterChatCommand('zp', zonepaste_func) zoneintersec_func = function(n) if not khCommandEnabled() then return end if khZoneOpsEnabled ~= nil and not khZoneOpsEnabled[0] then nakhodkaNotify(sName .. 'Операции с зоной выключены.', -1, 'info') return end if #n ~= 0 then local num = tonumber(n) if not num then nakhodkaNotify(sName .. "\xce\xf8\xe8\xe1\xea\xe0! \xc2\xe2\xe5\xe4\xe8 \xf7\xe8\xf1\xeb\xee, \xed\xe0\xef\xf0\xe8\xec\xe5\xf0: /zi 3", -1) return end local new_zone = zoneList:intersectLastN(num) if new_zone ~= nil then removeGangZone(610) left = new_zone.left up = new_zone.up right = new_zone.right down = new_zone.down addGangZone(610, new_zone.left, new_zone.up, new_zone.right, new_zone.down, -2130706433) khMarkZoneActive() nakhodkaNotify(sName .. "\xcf\xe5\xf0\xe5\xf1\xe5\xf7\xe5\xed\xe8\xe5 \xe7\xee\xed \xf3\xf1\xef\xe5\xf8\xed\xee \xed\xe0\xe9\xe4\xe5\xed\xee. Зона активна", -1) else nakhodkaNotify(sName .. "\xcf\xe5\xf0\xe5\xf1\xe5\xf7\xe5\xed\xe8\xe5 \xe7\xee\xed \xed\xe5 \xed\xe0\xe9\xe4\xe5\xed\xee", -1) end else nakhodkaNotify(sName .. "\xce\xf8\xe8\xe1\xea\xe0! \xc2\xe2\xe5\xe4\xe8 \xf7\xe8\xf1\xeb\xee, \xed\xe0\xef\xf0\xe8\xec\xe5\xf0: /zi 3", -1) end end sampRegisterChatCommand('zoneintersec', zoneintersec_func) sampRegisterChatCommand('zi', zoneintersec_func) zonerestore_func = function(n) if not khCommandEnabled() then return end if khZoneOpsEnabled ~= nil and not khZoneOpsEnabled[0] then nakhodkaNotify(sName .. 'Операции с зоной выключены.', -1, 'info') return end if #n ~= 0 then local num = tonumber(n) if not num then nakhodkaNotify(sName .. "\xce\xf8\xe8\xe1\xea\xe0! \xc2\xe2\xe5\xe4\xe8 \xf7\xe8\xf1\xeb\xee, \xed\xe0\xef\xf0\xe8\xec\xe5\xf0: /zr 1", -1) return end local new_zone = zoneList:getZoneFromEnd(num) if new_zone ~= nil then removeGangZone(610) left = new_zone.left up = new_zone.up right = new_zone.right down = new_zone.down addGangZone(610, new_zone.left, new_zone.up, new_zone.right, new_zone.down, -2130706433) khMarkZoneActive() nakhodkaNotify(sName .. "\xcf\xf0\xee\xf8\xeb\xe0\xff \xe7\xee\xed\xe0 \xe2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xeb\xe5\xed\xe0. Зона активна", -1) else nakhodkaNotify(sName .. "\xc7\xee\xed\xe0 \xed\xe5 \xed\xe0\xe9\xe4\xe5\xed\xe0", -1) end else nakhodkaNotify(sName .. "\xc2\xe2\xee\xe4\xe8 \xed\xee\xec\xe5\xf0 \xe7\xee\xed\xfb \xea\xee\xf2\xee\xf0\xf3\xfe \xed\xe0\xe4\xee \xe2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xe8\xf2\xfc", -1) end end sampRegisterChatCommand('zonerestore', zonerestore_func) sampRegisterChatCommand('zr', zonerestore_func) -- === \xcd\xce\xc2\xdb\xc5 \xca\xce\xcc\xc0\xcd\xc4\xdb \xc4\xcb\xdf \xd7\xc5\xca\xcf\xce\xc8\xcd\xd2\xc0 === metkacopy_func = function() if not khCommandEnabled() then return end local x, y, z = getCharCoordinates(PLAYER_PED) if x and y and z then local text = string.format("/mp %.4f %.4f %.4f", x, y, z) setClipboardText(text) nakhodkaNotify(sName .. '\xd2\xe2\xee\xe8 \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb \xf3\xf1\xef\xe5\xf8\xed\xee \xf1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xed\xfb!', -1) nakhodkaNotify(sName .. '\xce\xf2\xef\xf0\xe0\xe2\xfc \xfd\xf2\xee \xe4\xf0\xf3\xe3\xf3: ' .. text, -1) else nakhodkaNotify(sName .. '\xce\xf8\xe8\xe1\xea\xe0: \xed\xe5 \xf3\xe4\xe0\xeb\xee\xf1\xfc \xef\xee\xeb\xf3\xf7\xe8\xf2\xfc \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb \xef\xe5\xf0\xf1\xee\xed\xe0\xe6\xe0.', -1) end end sampRegisterChatCommand('metkacopy', metkacopy_func) sampRegisterChatCommand('mc', metkacopy_func) metkapaste_func = function(arg) if not khCommandEnabled() then return end if arg == nil or arg == "" then -- \xc5\xf1\xeb\xe8 \xef\xf0\xee\xf1\xf2\xee \xe2\xe2\xe5\xeb\xe8 /mp \xe1\xe5\xe7 \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2 \x97 \xf3\xe4\xe0\xeb\xff\xe5\xec \xe2\xf1\xb8 khClear3DPickups() khClearMainTreasureMarker() nakhodkaNotify(sName .. '\xd7\xe5\xea\xef\xee\xe8\xed\xf2 \xe8 \xec\xe5\xf2\xea\xe0 \xf3\xe4\xe0\xeb\xe5\xed\xfb!', -1) return end local pX, pY, pZ = string.match(arg, "(%-?[%d%.]+)%s+(%-?[%d%.]+)%s+(%-?[%d%.]+)") if pX and pY and pZ then pX, pY, pZ = tonumber(pX), tonumber(pY), tonumber(pZ) -- \xce\xf7\xe8\xf9\xe0\xe5\xec \xf1\xf2\xe0\xf0\xfb\xe9 \xf7\xe5\xea\xef\xee\xe8\xed\xf2, \xe5\xf1\xeb\xe8 \xee\xed \xe2\xe8\xf1\xe5\xeb khClear3DPickups() khClearMainTreasureMarker() -- 1. \xd1\xf2\xe0\xe2\xe8\xec \xec\xe5\xf2\xea\xf3 \xed\xe0 \xf0\xe0\xe4\xe0\xf0\xe5 placeWaypoint(pX, pY, pZ) -- 2. \xd1\xee\xe7\xe4\xe0\xe5\xec \xee\xe3\xf0\xee\xec\xed\xfb\xe9 3D \xea\xf0\xe0\xf1\xed\xfb\xe9 \xf6\xe8\xeb\xe8\xed\xe4\xf0 (\xf2\xe8\xef 1) activeCheckpointHandle = createCheckpoint(1, pX, pY, pZ, 0.0, 0.0, 0.0, activeMarkerRadius) -- \xd1\xee\xf5\xf0\xe0\xed\xff\xe5\xec \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb \xe4\xeb\xff \xf3\xe4\xe0\xeb\xe5\xed\xe8\xff \xef\xf0\xe8 \xe2\xf5\xee\xe4\xe5 activeMarkerCoord = {x = pX, y = pY, z = pZ} khActiveMainPointIndex = nil nakhodkaNotify(sName .. '\xd7\xe5\xea\xef\xee\xe8\xed\xf2 \xf3\xf1\xef\xe5\xf8\xed\xee \xf3\xf1\xf2\xe0\xed\xee\xe2\xeb\xe5\xed!', -1) nakhodkaNotify(sName .. '\xce\xed \xe0\xe2\xf2\xee\xec\xe0\xf2\xe8\xf7\xe5\xf1\xea\xe8 \xf3\xe4\xe0\xeb\xe8\xf2\xf1\xff, \xea\xee\xe3\xe4\xe0 \xf2\xfb \xe2 \xed\xe5\xe3\xee \xe2\xee\xe9\xe4\xe5\xf8\xfc.', -1) else nakhodkaNotify(sName .. '\xcd\xe5\xe2\xe5\xf0\xed\xfb\xe9 \xf4\xee\xf0\xec\xe0\xf2! \xc2\xf1\xf2\xe0\xe2\xfc \xf2\xee, \xf7\xf2\xee \xf1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xeb\xe0 \xea\xee\xec\xe0\xed\xe4\xe0 /mc', -1) end end sampRegisterChatCommand('metkapaste', metkapaste_func) sampRegisterChatCommand('mp', metkapaste_func) -- =============================== sampRegisterChatCommand('iskd', function(arg) if not khCommandEnabled() then return end if kdcond == true then nakhodkaNotify('{FF0000}[Nakhodka]{FFFFFF} \x96 \xd3 \xf2\xe5\xe1\xff \xea\xf3\xeb\xe4\xe0\xf3\xed. \xd2\xfb \xed\xe5 \xec\xee\xe6\xe5\xf8\xfc \xe8\xf1\xef\xee\xeb\xfc\xe7\xee\xe2\xe0\xf2\xfc \xea\xe0\xf0\xf2\xf3! \xd2\xe2\xee\xe9 \xea\xf3\xeb\xe4\xe0\xf3\xed \xea\xee\xed\xf7\xe8\xf2\xf1\xff ' .. endkd, -1) elseif kdcond == false then nakhodkaNotify('{3cb043}[Nakhodka]{FFFFFF} \x96 \xca\xf3\xeb\xe4\xe0\xf3\xed\xe0 \xed\xe5\xf2. \xcc\xee\xe6\xed\xee \xe8\xf1\xef\xee\xeb\xfc\xe7\xee\xe2\xe0\xf2\xfc \xea\xe0\xf0\xf2\xf3!', -1) end end) delgangzones = function(arg) if not khCommandEnabled() then return end -- \xd3\xe4\xe0\xeb\xff\xe5\xec \xe7\xee\xed\xfb, \xea\xee\xf2\xee\xf0\xfb\xe5 \xed\xe0\xec \xe8\xe7\xe2\xe5\xf1\xf2\xed\xfb (\xed\xe0\xea\xee\xef\xeb\xe5\xed\xfb \xe2 \xf2\xe0\xe1\xeb\xe8\xf6\xf3) for i = #gangZones, 1, -1 do removeGangZone(gangZones[i]) end -- \xce\xf7\xe8\xf9\xe0\xe5\xec \xf2\xe0\xe1\xeb\xe8\xf6\xf3, \xf7\xf2\xee\xe1\xfb \xed\xe5 \xf5\xf0\xe0\xed\xe8\xf2\xfc \xf3\xf1\xf2\xe0\xf0\xe5\xe2\xf8\xe8\xe5 ID gangZones = {} -- \xc4\xee\xef\xee\xeb\xed\xe8\xf2\xe5\xeb\xfc\xed\xee \xef\xf0\xee\xf5\xee\xe4\xe8\xec\xf1\xff \xef\xee \xe4\xe8\xe0\xef\xe0\xe7\xee\xed\xf3 ID \xe8 \xf3\xe4\xe0\xeb\xff\xe5\xec \xe2\xf1\xb8, -- \xf7\xf2\xee \xee\xf1\xf2\xe0\xeb\xee\xf1\xfc \xed\xe0 \xea\xe0\xf0\xf2\xe5 \xef\xee\xf1\xeb\xe5 \xef\xe5\xf0\xe5\xe7\xe0\xe3\xf0\xf3\xe7\xea\xe8 \xf1\xea\xf0\xe8\xef\xf2\xee\xe2 (\xea\xf0\xee\xec\xe5 \xed\xe0\xf8\xe5\xe9 \xe7\xee\xed\xfb 610) for id = 0, 1023 do if id ~= 610 then removeGangZone(id) end end nakhodkaNotify(sName .. '\xc2\xf1\xe5 \xeb\xe8\xf8\xed\xe8\xe5 \xe7\xee\xed\xfb \xe1\xe0\xed\xe4 \xf3\xe4\xe0\xeb\xe5\xed\xfb!', -1) end sampRegisterChatCommand('delgangzones', delgangzones) sampRegisterChatCommand('dgz', delgangzones) sampRegisterChatCommand('nakhodka', toggleNakhodkaMenu) sampRegisterChatCommand('kh', toggleNakhodkaMenu) local khStartNickname = khGetMyNickname() or 'игрок' nakhodkaNotify('Nakhodka Legal загружена. Приятного поиска кладов, ' .. khStartNickname, -1, 'success', 3) khDropHudReady = true khDropInvReady = false khDropInvCounts = {} khDropTempInvCounts = {} khDropExpectedItems = {} khDropPendingAddedItems = {} khDropRecentlyQueuedByAdded = {} khDropInvSyncMode = 'idle' khDropInvSyncState = KH_DROP_SYNC_IDLE khDropStatsSilentUntil = 0 khDropStatsHadDialog = false if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() if type(sampIsLocalPlayerSpawned) == 'function' then while not sampIsLocalPlayerSpawned() do wait(1000) end wait(3000) else wait(3000) end if khDropInvSyncState == KH_DROP_SYNC_IDLE then khStartDropInventorySync('initial') end end) end while true do wait(0) local now = os.clock() if khIsScriptEnabled() and now - khMainTreasureFastCheckAt >= 0.05 then khMainTreasureFastCheckAt = now if khUpdateMainTreasureCheckedFast(getCharCoordinates(PLAYER_PED)) then khMainTreasureLastRefresh = 0 end end if khIsScriptEnabled() and now - khMainTreasureLastRefresh >= 0.75 then khMainTreasureLastRefresh = now khRefreshMainTreasureBlips(false) end if khIsScriptEnabled() and now - khLastDopExpireCheck >= 30.0 then khLastDopExpireCheck = now khRemoveExpiredAdditionalPoints(true) end if khDropInvSyncState ~= KH_DROP_SYNC_IDLE and khDropStatsSilentUntil > 0 and now > khDropStatsSilentUntil then khDropTempInvCounts = {} khDropExpectedItems = {} khDropPendingAddedItems = {} khDropRecentlyQueuedByAdded = {} khDropInvSyncMode = 'idle' khDropInvSyncState = KH_DROP_SYNC_IDLE end khUpdatePendingDropSession(now) -- 1. \xcb\xee\xe3\xe8\xea\xe0 \xf2\xe0\xe9\xec\xe5\xf0\xe0 \xca\xf3\xeb\xe4\xe0\xf3\xed\xe0 local timenow = os.date('%X') if khIsScriptEnabled() and timenow == endkd and kdcond then printStyledString('~r~COOLDOWN END', 5000, 6) nakhodkaNotify('{3cb043}[Nakhodka]{FFFFFF} \x96 \xca\xf3\xeb\xe4\xe0\xf3\xed \xef\xf0\xee\xf8\xe5\xeb! \xca\xe0\xf0\xf2\xf3 \xec\xee\xe6\xed\xee \xe8\xf1\xef\xee\xeb\xfc\xe7\xee\xe2\xe0\xf2\xfc!', -1) kdcond = false endkd = '\xcd\xe5\xe8\xe7\xe2\xe5\xf1\xf2\xed\xee.' end -- 2. \xcb\xee\xe3\xe8\xea\xe0 \xef\xf0\xee\xef\xe0\xe4\xe0\xed\xe8\xff 3D-\xf7\xe5\xea\xef\xee\xe8\xed\xf2\xe0 \xef\xf0\xe8 \xe2\xf5\xee\xe4\xe5 if khIsScriptEnabled() then khCheckDopExpectation() end if khIsScriptEnabled() and activeMarkerCoord ~= nil then local charX, charY, charZ = getCharCoordinates(PLAYER_PED) if charX and charY then local dist = math.sqrt((charX - activeMarkerCoord.x)^2 + (charY - activeMarkerCoord.y)^2) if dist <= activeMarkerRadius then -- \xc8\xe3\xf0\xee\xea \xe7\xe0\xf8\xe5\xeb \xe2 \xf7\xe5\xea\xef\xee\xe8\xed\xf2 khClear3DPickups() khClearMainTreasureMarker() printStyledString('~g~CHECKPOINT REACHED', 3000, 4) nakhodkaNotify(sName .. '\xd2\xfb \xe4\xee\xe1\xf0\xe0\xeb\xf1\xff \xe4\xee \xf7\xe5\xea\xef\xee\xe8\xed\xf2\xe0! \xce\xed \xf3\xe4\xe0\xeb\xe5\xed.', -1) end end end end end function sampev.onShowDialog(id, style, title, button1, button2, text) local handled = khHandleDropStatsDialog(id, title or '', text or '') if handled == false then return false end if not khIsScriptEnabled() then return end end function sampev.onSetRaceCheckpoint(checkpointType, position, nextPosition, size) if not khIsScriptEnabled() then return end khRememberRaceCheckpoint(position) end function sampev.onServerMessage(color, message) local scriptEnabled = khIsScriptEnabled() khHandleDopServerMessage(message, not scriptEnabled) if not scriptEnabled then return end khHandlePreDigServerMessage(message) khHandleDropServerMessage(message) end function sampev.onCreateGangZone(zoneId, squareStart, squareEnd, color) if not khIsScriptEnabled() then return end if (khZoneOpsEnabled == nil or khZoneOpsEnabled[0]) and color == -16776961 then mapUsed = true kladZone = zoneId left = squareStart.x up = squareStart.y right = squareEnd.x down = squareEnd.y zoneList:addZone(Zone:new(left, up, right, down)) print('l: ' .. left .. '; u: ' .. up .. '; r: ' .. right .. '; d: ' .. down) removeGangZone(610) addGangZone(610, left, up, right, down, -2130706433) zoneActive = true khResetMainTreasureChecked() -- \xd1\xee\xf5\xf0\xe0\xed\xff\xe5\xec \xed\xee\xe2\xf3\xfe \xe7\xee\xed\xf3 \xe2 \xea\xee\xed\xf4\xe8\xe3 mainCfg.LastZone.left = left mainCfg.LastZone.up = up mainCfg.LastZone.right = right mainCfg.LastZone.down = down mainCfg.LastZone.saved = true inicfg.save(mainCfg, config_file) khRefreshMainTreasureBlips(true) khTeamQueueZoneNow() nakhodkaNotify(sName .. '\xd2\xe5\xf0\xf0\xe8\xf2\xee\xf0\xe8\xff \xed\xe0\xe9\xe4\xe5\xed\xe0! \xcf\xee\xf1\xeb\xe5 \xe5\xe5 \xe8\xf1\xf7\xe5\xe7\xed\xee\xe2\xe5\xed\xe8\xff \xee\xed\xe0 \xe1\xf3\xe4\xe5\xf2 \xe0\xe2\xf2\xee\xec\xe0\xf2\xe8\xf7\xe5\xf1\xea\xe8 \xe2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xeb\xe5\xed\xe0.', -1) nakhodkaNotify(sName .. '\xd7\xf2\xee\xe1\xfb \xf1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xf2\xfc \xe5\xe5 \xea\xee\xee\xf0\xe4\xe8\xed\xe0\xf2\xfb, \xef\xf0\xee\xef\xe8\xf8\xe8 /zonecopy', -1) nakhodkaNotify(sName .. '\xc4\xeb\xff \xed\xe0\xf5\xee\xe6\xe4\xe5\xed\xe8\xff \xef\xe5\xf0\xe5\xf1\xe5\xf7\xe5\xed\xe8\xe9 \xf1 \xef\xf0\xee\xf8\xeb\xfb\xec\xe8 \xe7\xee\xed\xe0\xec\xe8 \xe8\xf1\xef\xee\xeb\xfc\xe7\xf3\xe9\xf2\xe5 /zoneintersec N (/zi N), \xe3\xe4\xe5 N - \xea\xee\xeb\xe8\xf7\xe5\xf1\xf2\xe2\xee \xef\xee\xf1\xeb\xe5\xe4\xed\xe8\xf5 \xe7\xee\xed', -1) nakhodkaNotify(sName .. '\xc4\xeb\xff \xe2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xeb\xe5\xed\xe8\xff \xef\xf0\xee\xf8\xeb\xfb\xf5 \xe7\xee\xed \xe8\xf1\xef\xee\xeb\xfc\xe7\xf3\xe9\xf2\xe5 /zonerestore N (/zr N), \xe3\xe4\xe5 N - \xed\xee\xec\xe5\xf0 \xe7\xee\xed\xfb \xf1 \xea\xee\xed\xf6\xe0', -1) return false -- < \xe2\xee\xf2 \xfd\xf2\xee \xe3\xeb\xe0\xe2\xed\xee\xe5, \xe1\xeb\xee\xea\xe8\xf0\xf3\xe5\xf2 \xf1\xee\xe7\xe4\xe0\xed\xe8\xe5 \xf1\xe8\xed\xe5\xe9/\xea\xf0\xe0\xf1\xed\xee\xe9 \xe7\xee\xed\xfb else table.insert(gangZones, zoneId) end end function sampev.onGangZoneDestroy(zoneId1) if not khIsScriptEnabled() or khZoneOpsEnabled == nil or not khZoneOpsEnabled[0] then return end if zoneId1 == kladZone then removeGangZone(610) addGangZone(610, left, up, right, down, -2130706433) zoneActive = true khResetMainTreasureChecked() khRefreshMainTreasureBlips(true) nakhodkaNotify(sName .. '\xd2\xe5\xf0\xf0\xe8\xf2\xee\xf0\xe8\xff \xe2\xee\xe7\xe2\xf0\xe0\xf9\xe5\xed\xe0! \xce\xf2\xf1\xf7\xb8\xf2 \xea\xf3\xeb\xe4\xe0\xf3\xed\xe0 \xe7\xe0\xef\xf3\xf9\xe5\xed!', -1) timekd = os.date('%X') hourkd, minutekd, secundkd = timekd:match('(%d+):(%d+):(%d+)') if minutekd + 30 < 60 then endkd = hourkd .. ':' .. tonumber(minutekd) + 30 .. ':' .. secundkd else endkd = hourkd + 1 .. ':' .. tonumber(minutekd) - 60 + 30 .. ':' .. secundkd end kdcond = true end end function addGangZone(id, left, up, right, down, color) local bs = raknetNewBitStream() raknetBitStreamWriteInt16(bs, id) raknetBitStreamWriteFloat(bs, left) raknetBitStreamWriteFloat(bs, up) raknetBitStreamWriteFloat(bs, right) raknetBitStreamWriteFloat(bs, down) raknetBitStreamWriteInt32(bs, color) raknetEmulRpcReceiveBitStream(108, bs) raknetDeleteBitStream(bs) end function removeGangZone(id) local bs = raknetNewBitStream() raknetBitStreamWriteInt16(bs, id) raknetEmulRpcReceiveBitStream(120, bs) raknetDeleteBitStream(bs) end local function khCleanupScriptArtifacts() pcall(khClear3DPickups) pcall(khClearMainTreasureBlips) pcall(khClearAdditionalBlips) pcall(khClearActiveDopMarker) pcall(khClearMainTreasureMarker) pcall(khClearTeamMapArtifacts) if type(removeGangZone) == 'function' then pcall(removeGangZone, 610) end end function onScriptTerminate(script, quitGame) if type(thisScript) ~= 'function' or script == thisScript() then pcall(khWriteRuntimeCleanupArtifacts) -- Ctrl+R unload can crash the game if native marker/blip/gangzone removal -- happens while MoonLoader is already tearing the script down. Runtime cleanup -- is handled by normal toggles and the next script start, so termination stays passive. kh3DForceClear = false kh3DPickupMarkers = {} khMainTreasureBlips = {} khAdditionalBlips = {} khTeamMemberBlips = {} khTeamMemberZones = {} activeCheckpointHandle = nil khDopActiveCheckpointHandle = nil activeMarkerCoord = nil khDopActivePointId = nil end end