-- Nakhodka rebuild by Codex & Ferkomen script_author('Codex & Ferkomen') script_name('nakhodka') NK_BOOT_BASE_URL = 'https://nakhodka.fun/files/' NK_BOOT_FALLBACK_BASE_URL = nil NK_SCRIPT_VERSION = 2026080901 NK_SCRIPT_VERSION_TEXT = '2026.08.09.1' --[[NK_BUILD_CONFIG_START]] NK_BUILD_MODE = 'legal' NK_BUILD_FEATURES = { markers3d = false, pointcheck = false, team = false, teamMap = false, teamMapNames = false, teamMapZones = false, teamWorldZones = false, companion = true, dopki = true, digCursor = true, } --[[NK_BUILD_CONFIG_END]] function khFeatureEnabled(name) if type(NK_BUILD_FEATURES) ~= 'table' then return true end return NK_BUILD_FEATURES[tostring(name or '')] ~= false end NK_UPDATE_SCRIPT_URLS = { 'https://nakhodka.fun/build?mode=legal' } NK_UPDATE_MIN_SIZE = 1 NK_SIGNED_UPDATE_REQUIRED = false NK_ED25519_PUBLIC_KEY_B64 = '' 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) local relative = tostring(rel or ''):gsub(NK_BOOT_SEP, '/') local bases = {NK_BOOT_BASE_URL} for _, base in ipairs(bases) do base = tostring(base or '') if base ~= '' and nkBootDownloadUrl(base .. relative, path, minSize, false) then return true end end return false end function nkBootDownloadUrlViaRequests(url, path, minSize) minSize = tonumber(minSize) or 1 local okReq, requests = pcall(require, 'requests') if not okReq or requests == nil then return false end local okResp, response = pcall(function() if type(requests.request) == 'function' then return requests.request('GET', tostring(url or ''), {timeout = 30, headers = {['User-Agent'] = 'Nakhodka'}}) elseif type(requests.get) == 'function' then return requests.get(tostring(url or '')) end return nil end) if not okResp or type(response) ~= 'table' then return false end local statusCode = tonumber(response.status_code or response.status) if statusCode ~= nil and (statusCode < 200 or statusCode >= 300) then return false end local body = response.text or response.content or response.body or response.data if type(body) ~= 'string' or #body < minSize then return false end if not nkBootWriteAll(path, body) then return false end return nkBootFileSize(path) >= minSize end function nkBootDownloadUrl(url, path, minSize, allowBlockingFallback) minSize = tonumber(minSize) or 1 nkBootEnsureDirFor(path) url = tostring(url or '') if not url:match('^https://') then return false end if type(downloadUrlToFile) == 'function' then 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 startedOk then while not done and os.clock() - started < 12 do local size = nkBootFileSize(path) if size >= minSize and size == lastSize and os.clock() - stableAt > 0.35 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 if ok and nkBootFileSize(path) >= minSize then return true end end end if allowBlockingFallback then return nkBootDownloadUrlViaRequests(url, path, minSize) end return false end -- A checksum detects accidental damage. It does not authenticate a download; -- executable updates additionally require an Ed25519 signature verifier. function nkBootSha256Data(data) local ok, crypto = pcall(require, 'crypto_lua') if not ok or type(crypto) ~= 'table' or type(crypto.sha256) ~= 'function' then return nil end local hashed, digest = pcall(crypto.sha256, tostring(data or '')) if not hashed or type(digest) ~= 'string' or not digest:match('^[0-9A-Fa-f][0-9A-Fa-f]+$') or #digest ~= 64 then return nil end return digest:upper() end function nkBootSignatureVerifierAvailable() if not NK_SIGNED_UPDATE_REQUIRED then return true end if tostring(NK_ED25519_PUBLIC_KEY_B64 or '') == '' then return false end local ok, verifier = pcall(require, 'nakhodka_ed25519') return ok and type(verifier) == 'table' and type(verifier.verify) == 'function' end function nkBootSignatureVerifier() if not NK_SIGNED_UPDATE_REQUIRED then return nil end local ok, verifier = pcall(require, 'nakhodka_ed25519') if not ok or type(verifier) ~= 'table' or type(verifier.verify) ~= 'function' then return nil end return verifier end function nkBootBuildFeaturesJson(text) text = tostring(text or '') local names = {'markers3d', 'pointcheck', 'team', 'teamMap', 'teamMapNames', 'teamMapZones', 'teamWorldZones', 'companion'} local block = text:match('NK_BUILD_FEATURES%s*=%s*%{(.-)%}') if block == nil then return nil end local parts = {} for _, name in ipairs(names) do local raw = tostring(block:match(name .. '%s*=%s*(%a+)') or ''):lower() if raw ~= 'true' and raw ~= 'false' then return nil end parts[#parts + 1] = string.format('"%s":%s', name, raw) end return '{' .. table.concat(parts, ',') .. '}' end function nkBootBuildManifestUrl(buildUrl) local url = tostring(buildUrl or '') local from, to = url:find('/build', 1, true) if from == nil then return nil end local nextChar = url:sub(to + 1, to + 1) if nextChar ~= '' and nextChar ~= '?' then return nil end return url:sub(1, from - 1) .. '/build-manifest' .. url:sub(to + 1) end function nkBootManifestField(text, name) name = tostring(name or ''):gsub('([^%w])', '%%%1') return tostring(text or ''):match('"' .. name .. '"%s*:%s*"([^"\\]*)"') end function nkBootManifestNumber(text, name) name = tostring(name or ''):gsub('([^%w])', '%%%1') return tonumber(tostring(text or ''):match('"' .. name .. '"%s*:%s*(%d+)')) end function nkBootReadBuildManifest(buildUrl, tempPath) local manifestUrl = nkBootBuildManifestUrl(buildUrl) if manifestUrl == nil or not manifestUrl:match('^https://') then return nil, 'manifest_url' end local path = tostring(tempPath or '') .. '.manifest.tmp' pcall(os.remove, path) if not nkBootDownloadUrl(manifestUrl, path, 64, false) then pcall(os.remove, path) return nil, 'manifest_download' end local text = nkBootReadAll(path) pcall(os.remove, path) if type(text) ~= 'string' then return nil, 'manifest_read' end local manifest = { protocol = nkBootManifestField(text, 'protocol'), version = nkBootManifestNumber(text, 'version'), mode = nkBootManifestField(text, 'mode'), sha256 = nkBootManifestField(text, 'sha256'), signature = nkBootManifestField(text, 'signature'), keyId = nkBootManifestField(text, 'key_id') } if manifest.protocol ~= 'nakhodka-build-v1' or type(manifest.version) ~= 'number' or not tostring(manifest.mode or ''):match('^[a-z]+$') or not tostring(manifest.sha256 or ''):match('^[0-9A-Fa-f]+$') or #tostring(manifest.sha256 or '') ~= 64 or tostring(manifest.signature or '') == '' then return nil, 'manifest_invalid' end return manifest end function nkBootSignedEnvelope(version, mode, featuresJson, sha256) return 'nakhodka-build-v1\n' .. tostring(version) .. '\n' .. tostring(mode) .. '\n' .. tostring(featuresJson) .. '\n' .. tostring(sha256):lower() .. '\n' end function nkBootVerifySignedBuild(remoteText, manifest) if not NK_SIGNED_UPDATE_REQUIRED then return true end local verifier = nkBootSignatureVerifier() if verifier == nil then return false, 'signature_verifier_unavailable' end local bodySha256 = nkBootSha256Data(remoteText) if bodySha256 == nil or bodySha256:lower() ~= tostring(manifest.sha256 or ''):lower() then return false, 'sha256' end local version = nkExtractScriptVersion(remoteText) local mode = select(1, nkBootBuildSignature(remoteText)) local featuresJson = nkBootBuildFeaturesJson(remoteText) if version < 1 or version ~= tonumber(manifest.version) or mode ~= tostring(manifest.mode or '') or featuresJson == nil then return false, 'manifest_mismatch' end local envelope = nkBootSignedEnvelope(version, mode, featuresJson, bodySha256) local ok, verified = pcall(verifier.verify, envelope, tostring(manifest.signature or ''), tostring(NK_ED25519_PUBLIC_KEY_B64 or '')) if not ok or verified ~= true then return false, 'signature' end return true 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 nkBootLooksLikeScriptPath(path) path = tostring(path or '') if path == '' then return false end local lower = path:lower() return lower:match('%.lua$') ~= nil or lower:match('%.luac$') ~= nil end function nkBootScriptField(script, key) if script == nil then return nil end local ok, value = pcall(function() local item = script[key] if type(item) == 'function' then return item(script) end return item end) if ok and value ~= nil and tostring(value) ~= '' then return tostring(value) end return nil end function nkBootNormalizeSelfPath(path) path = tostring(path or ''):gsub('/', NK_BOOT_SEP) if path == '' then return nil end if nkBootLooksLikeScriptPath(path) then return path end return nil end function nkBootSelfPath() local candidates = {} if type(thisScript) == 'function' then local ok, script = pcall(thisScript) if ok and script ~= nil then for _, key in ipairs({'path', 'filepath', 'file', 'filename'}) do local value = nkBootScriptField(script, key) if value ~= nil then candidates[#candidates + 1] = value end end local dir = nkBootScriptField(script, 'directory') or nkBootScriptField(script, 'dir') or nkBootScriptField(script, 'path') local file = nkBootScriptField(script, 'filename') or nkBootScriptField(script, 'file') if dir ~= nil and file ~= nil and not nkBootLooksLikeScriptPath(dir) then candidates[#candidates + 1] = nkBootJoin(dir, file) end end end 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 candidates[#candidates + 1] = source:sub(2) end for _, candidate in ipairs(candidates) do local path = nkBootNormalizeSelfPath(candidate) if path ~= nil then return path end end return nkBootJoin(nkBootWorkDir(), 'Nakhodka.lua') end function nkBootBackupPath(path) return tostring(path or '') .. '.bak-update-' .. os.date('%Y%m%d-%H%M%S') end function nkBootCleanupUpdateBackups() -- Do not spawn cmd.exe during script load. Old update backups are harmless -- and can be cleaned by the updater without blocking the game. return true end function nkExtractScriptVersion(text) return tonumber(tostring(text or ''):match('NK_SCRIPT_VERSION%s*=%s*(%d+)')) or 0 end function nkBootBuildSignature(text) text = tostring(text or '') local mode = text:match("NK_BUILD_MODE%s*=%s*'([^']+)'") or text:match('NK_BUILD_MODE%s*=%s*"([^"]+)"') or '' local block = text:match('NK_BUILD_FEATURES%s*=%s*%{(.-)%}') if block == nil then return mode, '' end local names = {'markers3d', 'pointcheck', 'team', 'teamMap', 'teamMapNames', 'teamMapZones', 'teamWorldZones', 'companion'} local parts = {} for _, name in ipairs(names) do local raw = block:match(name .. '%s*=%s*(%a+)') parts[#parts + 1] = name .. '=' .. tostring(raw or ''):lower() end return mode, table.concat(parts, ';') end function nkBootCheckSelfUpdate() -- Never replace executable Lua until a real Ed25519 verifier is bundled. -- Size checks and string matching are intentionally not a trust decision. if NK_SIGNED_UPDATE_REQUIRED and not nkBootSignatureVerifierAvailable() then return false, false, 'signature_verifier_unavailable' end local selfPath = nkBootSelfPath() local tempPath = nkBootJoin(nkBootWorkDir(), 'config/nakhodka_update.tmp') local urls = NK_UPDATE_SCRIPT_URLS or {} local downloaded = false local manifest = nil for _, baseUrl in ipairs(urls) do local sep = tostring(baseUrl):find('?', 1, true) and '&' or '?' local requestUrl = tostring(baseUrl) .. sep .. 't=' .. tostring(os.time()) if nkBootDownloadUrl(requestUrl, tempPath, NK_UPDATE_MIN_SIZE or 1, false) then local parsed = nkBootReadBuildManifest(requestUrl, tempPath) if parsed ~= nil then manifest = parsed downloaded = true break end pcall(os.remove, tempPath) 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')", 1, true) then return false, false, 'bad_file' end local signed, signatureError = nkBootVerifySignedBuild(remoteText, manifest) if not signed then return false, false, signatureError or 'signature' end local localMode, localBuildSig = nkBootBuildSignature(localText) local remoteMode, remoteBuildSig = nkBootBuildSignature(remoteText) if localBuildSig ~= '' and remoteBuildSig ~= '' and (localMode ~= remoteMode or localBuildSig ~= remoteBuildSig) then return true, false, 'build_mismatch' 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, remoteVersion < localVersion and 'older' or 'same' end nkBootCleanupUpdateBackups() 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, checksum = line:match('^([^|]+)|(%d+)|([0-9A-Fa-f]+)$') if rel == nil then rel, minSize = line:match('^([^|]+)|(%d+)$') end if rel ~= nil then items[#items + 1] = {rel = rel:gsub(NK_BOOT_SEP, '/'), minSize = tonumber(minSize) or 1, checksum = checksum} end end end file:close() return items end function nkBootFileChecksum(path) local file = io.open(path, 'rb') if not file then return nil end local data = file:read('*a') file:close() if type(data) ~= 'string' then return nil end return nkBootSha256Data(data) end function nkBootNeedsFile(path, minSize, checksum) if nkBootFileSize(path) < (tonumber(minSize) or 1) then return true end checksum = tostring(checksum or '') if checksum ~= '' then local actual = nkBootFileChecksum(path) return tostring(actual or ''):upper() ~= checksum:upper() end return false end function nkBootIsLegacyMarksRel(rel) rel = tostring(rel or ''):gsub('\\\\', '/'):lower() return rel == 'config/nakhodka_marks.dat' or rel == 'config/nakhodka_marks.dat.nkhd2-backup' or rel == 'config/nakhodka_marks.v3.dat' or rel == 'config/nakhodka_marks.json' or rel == 'resource/nk_7b41a9.cache' end function nkBootProtectedMarksRel() return 'resource/nakhodka.cashe' end function nkBootstrapFiles() -- Bootstrap downloads Lua/DLL dependencies. Refuse this unsafe path until -- its manifest and artifacts have a signed protocol of their own. A build -- signature does not authenticate every separate bootstrap artifact. if NK_SIGNED_UPDATE_REQUIRED then print('Nakhodka bootstrap: signed bootstrap manifest is required.') return -1 end local base = nkBootWorkDir() local manifestPath = nkBootJoin(base, 'config/nakhodka_manifest.txt') local manifestOk, items = false, {} for attempt = 1, 3 do manifestOk = nkBootDownload('manifest.txt', manifestPath, 10) items = nkBootReadManifest(manifestPath) if manifestOk and type(items) == 'table' and #items > 0 then break end if attempt < 3 and type(wait) == 'function' then wait(500) end end if not manifestOk or type(items) ~= 'table' or #items == 0 then print('Nakhodka bootstrap: manifest is missing or empty.') return -1 end local downloaded, failed = 0, 0 for _, item in ipairs(items) do local path = nkBootJoin(base, item.rel) local relLower = tostring(item.rel or ''):gsub('\\', '/'):lower() if nkBootIsLegacyMarksRel(item.rel) then pcall(os.remove, path) elseif relLower == 'lib/cef-lite.lua' then -- cef-lite is compared by content, like KladMap, instead of only by size. -- The temporary file prevents a failed download from damaging a working copy. local tempPath = path .. '.download.tmp' local remoteOk = nkBootDownload(item.rel, tempPath, item.minSize) local remoteText = remoteOk and nkBootReadAll(tempPath) or nil local localText = nkBootReadAll(path) if remoteText ~= nil and #remoteText >= (tonumber(item.minSize) or 1) then if localText ~= remoteText then if nkBootWriteAll(path, remoteText) then downloaded = downloaded + 1 else failed = failed + 1 end end pcall(os.remove, tempPath) elseif localText == nil or #localText < (tonumber(item.minSize) or 1) then failed = failed + 1 pcall(os.remove, tempPath) else pcall(os.remove, tempPath) end elseif relLower == nkBootProtectedMarksRel() then -- Основные метки никогда не обновляются через bootstrap/check files. -- Их единственный источник - coords.json и кнопка "Перезагрузить точки". elseif nkBootNeedsFile(path, item.minSize, item.checksum) 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)) if downloaded <= 0 then return -failed end end return downloaded end function nkBootRemoveLegacyMarks() local base = nkBootWorkDir() local paths = { nkBootJoin(base, 'config/nakhodka_marks.dat'), nkBootJoin(base, 'config/nakhodka_marks.dat.nkhd2-backup'), nkBootJoin(base, 'config/nakhodka_marks.v3.dat'), nkBootJoin(base, 'config/nakhodka_marks.json'), nkBootJoin(base, 'resource/nk_7b41a9.cache') } for _, path in ipairs(paths) do pcall(os.remove, path) end end function nkBootNeedsFirstInstall() local base = nkBootWorkDir() local required = { 'lib/samp/events.lua', 'lib/inicfg.lua', 'lib/encoding.lua', 'lib/dkjson.lua', 'lib/mimgui/init.lua', 'lib/effil.lua', 'lib/cef-lite.lua', 'lib/libeffil.dll', 'lib/requests.lua', 'lib/notify.lua', 'lib/nakhodka_route.lua', 'config/nakhodka_phrases.json', 'resource/nakhodka_tyan.png', 'resource/nakhodka_marat_crop.png', 'resource/nakhodka_map.png', 'resource/nakhodka_vc.png' } for _, rel in ipairs(required) do if nkBootFileSize(nkBootJoin(base, rel)) < 1 then return true end end return false end function nkBootStartFirstInstall() if not nkBootNeedsFirstInstall() then return false end if type(lua_thread) ~= 'table' or type(lua_thread.create) ~= 'function' then print('Nakhodka bootstrap: lua_thread is unavailable.') nkBootInstallFinished = true nkBootInstallResult = -1 return true end if nkBootInstallStarted then return true end nkBootInstallStarted = true nkBootInstallFinished = false nkBootInstallResult = -1 lua_thread.create(function() local ok, result = pcall(nkBootstrapFiles) result = (ok and tonumber(result)) or -1 nkBootInstallResult = result nkBootInstallFinished = true if result < 0 then print('Nakhodka bootstrap: download failed.') if nkBootFileSize(nkBootJoin(nkBootWorkDir(), 'lib/cef-lite.lua')) < 1 then print('Не удалось скачать cef-lite.lua. Обратитесь к разработчику.') end else print('Nakhodka bootstrap: files downloaded, waiting for script reload.') end end) return true end function nkBootStartBackgroundSync() if nkBootBackgroundSyncStarted then return end if type(lua_thread) ~= 'table' or type(lua_thread.create) ~= 'function' then return end nkBootBackgroundSyncStarted = true lua_thread.create(function() local ok, result = pcall(nkBootstrapFiles) result = ok and tonumber(result) or -1 if result > 0 and type(thisScript) == 'function' then wait(250) local script = thisScript() if script ~= nil then pcall(function() script:reload() end) end end end) end -- A clean install has only this Lua file. The bootstrap worker does the I/O; -- the temporary main coroutine waits and reloads after the chunk is loaded. if nkBootStartFirstInstall() then local nkBootstrapScript = nil if type(thisScript) == 'function' then local okScript, currentScript = pcall(thisScript) if okScript then nkBootstrapScript = currentScript end end function main() while not nkBootInstallFinished do wait(0) end if tonumber(nkBootInstallResult) and nkBootInstallResult >= 0 then if type(sampAddChatMessage) == 'function' then pcall(sampAddChatMessage, '{B84DFF}[Nakhodka]{FFFFFF} Файлы загружены. Запускаю свежую копию...', -1) end wait(200) local loaded = false local selfPath = nkBootSelfPath() if type(script) == 'table' and type(script.load) == 'function' and selfPath ~= nil then local okLoad, newScript = pcall(function() return script.load(selfPath) end) loaded = okLoad and newScript ~= nil end if loaded then wait(100) if nkBootstrapScript ~= nil then pcall(function() nkBootstrapScript:unload() end) end elseif nkBootstrapScript ~= nil then pcall(function() nkBootstrapScript:reload() end) elseif type(reloadScripts) == 'function' then reloadScripts() end end end return end nkBootStartBackgroundSync() pcall(nkBootRemoveLegacyMarks) pcall(nkBootCleanupUpdateBackups) 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' pcall(ffi.cdef, [[ void *ShellExecuteA(void *hwnd, const char *operation, const char *file, const char *parameters, const char *directory, int show); typedef struct NK_OPENFILENAMEA { unsigned long lStructSize; void *hwndOwner; void *hInstance; const char *lpstrFilter; char *lpstrCustomFilter; unsigned long nMaxCustFilter; unsigned long nFilterIndex; char *lpstrFile; unsigned long nMaxFile; char *lpstrFileTitle; unsigned long nMaxFileTitle; const char *lpstrInitialDir; const char *lpstrTitle; unsigned long Flags; unsigned short nFileOffset; unsigned short nFileExtension; const char *lpstrDefExt; intptr_t lCustData; void *lpfnHook; const char *lpTemplateName; } NK_OPENFILENAMEA; int GetOpenFileNameA(NK_OPENFILENAMEA *ofn); typedef struct NK_POINT { long x; long y; } NK_POINT; typedef struct NK_RECT { long left; long top; long right; long bottom; } NK_RECT; void *GetForegroundWindow(void); int GetClientRect(void *hwnd, NK_RECT *rect); int ClientToScreen(void *hwnd, NK_POINT *point); int SetCursorPos(int x, int y); int ClipCursor(const NK_RECT *rect); short GetAsyncKeyState(int vKey); ]]) khUser32 = nil pcall(function() khUser32 = ffi.load('user32') end) khShell32 = nil pcall(function() khShell32 = ffi.load('shell32') end) khComdlg32 = nil pcall(function() khComdlg32 = ffi.load('comdlg32') end) local khBitReady, khBit = pcall(require, 'bit') if not khBitReady then khBit = nil end khSocketReady, khSocket = pcall(require, 'socket') -- dkjson is pure Lua and does not re-enter MoonLoader's C coroutine state. -- cjson used to crash during concurrent network/marks processing. khJsonReady, khJson = pcall(require, 'dkjson') if not khJsonReady then khJson = nil end khRequestsReady, khRequests = pcall(require, 'requests') khEffilReady, khEffil = pcall(require, 'effil') khCefReady, khCef = pcall(require, 'lib.cef-lite') if not khCefReady then khCef = nil print('Не удалось подключить cef-lite.lua. Подсчёт дропа отключён.') if type(sampAddChatMessage) == 'function' then pcall(sampAddChatMessage, '{B84DFF}[Nakhodka]{FFFFFF} Не удалось подключить cef-lite.lua. Обратитесь к разработчику.', -1) end end 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 khTitleReloadClicks = 0 khTitleReloadLastAt = 0 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} local khFixedDigCursorEnabled = imguiReady and new.bool(true) or {[0] = true} local khHideServerIcons = imguiReady and new.bool(false) or {[0] = false} local khNotificationsEnabled = imguiReady and new.bool(true) or {[0] = true} local khNotificationSettingsOpen = imguiReady and new.bool(false) or {[0] = false} local khNotifyCategories = { map = imguiReady and new.bool(true) or {[0] = true}, zones = imguiReady and new.bool(true) or {[0] = true}, points = imguiReady and new.bool(true) or {[0] = true}, drops = imguiReady and new.bool(true) or {[0] = true}, team = imguiReady and new.bool(true) or {[0] = true}, updates = imguiReady and new.bool(true) or {[0] = true}, companion = imguiReady and new.bool(true) or {[0] = true}, system = imguiReady and new.bool(true) or {[0] = true} } local khDisableZoneBlink = imguiReady and new.bool(true) or {[0] = true} 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 = {} khCustomCompanions = {} khCustomActiveId = nil khCustomSelectedId = nil khCustomNameInput = imguiReady and new.char[96]('') or nil khCustomPhrasesInput = imguiReady and new.char[16384]('') or nil khCustomCelebrateInput = imguiReady and new.char[16384]('') or nil khCustomHelloInput = imguiReady and new.char[256]('') or nil khCustomMoveInput = imguiReady and new.char[256]('') or nil khCustomSavedInput = imguiReady and new.char[256]('') or nil khCustomImageInput = imguiReady and new.char[320]('') or nil khCustomEditorId = nil khCustomCreateSerial = 0 khCustomWindowOpen = imguiReady and new.bool(false) or {[0] = false} 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}, titleBg = {clamp(0.025 + r * 0.045), clamp(0.020 + g * 0.035), clamp(0.040 + b * 0.045), 0.98}, titleBgActive = {clamp(0.085 + r * 0.25), clamp(0.060 + g * 0.20), clamp(0.12 + b * 0.28), 1.00}, titleBgCollapsed = {clamp(0.040 + r * 0.08), clamp(0.030 + g * 0.06), clamp(0.060 + b * 0.08), 0.96}, 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(443) or {[0] = 443} khTeam = { host = 'nakhodka.fun', port = 443, apiUrl = 'https://nakhodka.fun/team/poll', 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, lastErrorAt = 0, lastSosAt = 0, lastInviteAt = 0, ownSosActive = false, ownSosUntil = 0, lastX = nil, lastY = nil, lastZ = nil, socket = nil, httpInFlight = false, httpLastHelloAt = 0, httpReconnectEpoch = 0, lastManualReconnectAt = 0 } khTeamOutgoing = {} khTeamIncoming = {} khTeamInvites = {} khTeamMembers = {} khTeamRoster = {} khTeamRosterVisible = false khTeamRosterLastRequestAt = 0 khTeamRosterLastSeenAt = 0 khTeamRosterTtl = 20.0 khTeamMemberBlips = {} khTeamMemberZones = {} khTeamZoneIds = {} khTeamTargetCheckpointHandle = nil khTeamTarget = nil khTeamMapTexture = nil khTeamMapTextureTried = false khTeamMapTextures = {} khTeamMapTextureTriedByKind = {} khTeamMapTextureKind = nil khTeamMapFont = nil khTeamMapSmallFont = nil khTeamOwnZone = {active = false} khTeamThreadStarted = false khTeamNetworkThreadActive = false khTeamLastZoneFingerprint = '' khTeamInviteInput = imguiReady and new.char[32]('') or nil khTeamServerInput = imguiReady and new.char[32]('') or nil khTeamChatInput = imguiReady and new.char[1024]('') or nil khTeamSelectedMemberToken = nil khTeamMapEnabled = imguiReady and new.bool(true) or {[0] = true} khTeamMapHoldMode = imguiReady and new.bool(false) or {[0] = false} khTeamMapTransparency = imguiReady and new.int(0) or {[0] = 0} khTeamShowNearby = imguiReady and new.bool(false) or {[0] = false} khTeamMapKey = 0x47 khTeamMapKeyWaiting = false khTeamMapSideKeyState = {[0x05] = false, [0x06] = false} khTeamMapOpen = false khTeamMapCursorOwned = false khTeamMapDragging = false khTeamMapDragOffset = {x = 0, y = 0} khTeamMapPos = {x = nil, y = nil} khTeamMapSize = 0 local khRoadOk, khRoad = pcall(require, 'nakhodka_route') if not khRoadOk or type(khRoad) ~= 'table' then khRoad = nil end local khRoadState = {target=nil, path={}, job=nil, origin=nil, status='idle'} khTeamChatVisible = false khTeamEmojiOpen = false khTeamChatLogs = {} khTeamChatSeen = {} khTeamChatNeedsScroll = false khTeamChatTeamId = '' khTeamLocalIdCache = {} khTeamLocalIdCacheAt = 0 khResetSettingsConfirm = false khUninstallConfirm = false khUnloadKey = 0 khUnloadKeyWaiting = false khUnloadBusy = false 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 function khIsScriptEnabled() return khScriptEnabled == nil or khScriptEnabled[0] end function khCommandEnabled() if khIsScriptEnabled() then return true end nakhodkaNotify(sName .. 'Скрипт выключен. Включи его в /kh -> Основное.', -1, 'info') return false end function khTeamCommandEnabled() if not khCommandEnabled() then return false end if not khFeatureEnabled('team') then nakhodkaNotify('Командный модуль отключен в этой сборке Nakhodka.', -1, 'info', 3) return false end if khTeamEnabled ~= nil and not khTeamEnabled[0] then nakhodkaNotify('Командный модуль выключен. Включи команду в /kh -> Команда.', -1, 'info', 3) return false end return true end 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 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 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 khDetectNotifyCategory(text) local value = tostring(text or ''):gsub('.', function(char) local byte = string.byte(char) if byte == 168 then return string.char(184) end if byte ~= nil and byte >= 192 and byte <= 223 then return string.char(byte + 32) end return char end):lower() if value:find('территория найдена', 1, true) or (value:find('карт', 1, true) and (value:find('актив', 1, true) or value:find('использ', 1, true))) then return 'map' end if value:find('зон', 1, true) or value:find('территор', 1, true) or value:find('кулдаун', 1, true) or value:find('cooldown', 1, true) then return 'zones' end if value:find('команд', 1, true) or value:find('sos', 1, true) or value:find('союзник', 1, true) or value:find('gps', 1, true) then return 'team' end if value:find('дроп', 1, true) or value:find('прибыл', 1, true) or value:find('цен', 1, true) or value:find('статист', 1, true) then return 'drops' end if value:find('клад', 1, true) or value:find('точк', 1, true) or value:find('допк', 1, true) or value:find('чекпоинт', 1, true) or value:find('метк', 1, true) then return 'points' end if value:find('обнов', 1, true) or value:find('загруж', 1, true) or value:find('файл', 1, true) then return 'updates' end if value:find('спутник', 1, true) or value:find('тяноч', 1, true) or value:find('марат', 1, true) then return 'companion' end return 'system' end function nakhodkaNotify(text, color, notifyType, duration, category) if khNotificationsEnabled ~= nil and not khNotificationsEnabled[0] then return end local selectedCategory = category or khDetectNotifyCategory(clearNotifyText(text)) local categoryToggle = khNotifyCategories ~= nil and khNotifyCategories[selectedCategory] or nil if categoryToggle ~= nil and not categoryToggle[0] then return end 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 khHandleTitleReloadClick() local nowClock = os.clock() if nowClock - (tonumber(khTitleReloadLastAt) or 0) > 1.8 then khTitleReloadClicks = 0 end khTitleReloadLastAt = nowClock khTitleReloadClicks = (tonumber(khTitleReloadClicks) or 0) + 1 if khTitleReloadClicks >= 4 then khTitleReloadClicks = 0 nakhodkaNotify('Перезагружаю Nakhodka...', -1, 'info', 2) if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() wait(120); khReloadThisScript() end) else khReloadThisScript() end end end function khOpenExternalUrl(url) url = tostring(url or '') if url == '' then return end if khShell32 ~= nil then pcall(khShell32.ShellExecuteA, nil, 'open', url, nil, nil, 1) end end function khRunHiddenFile(path) path = tostring(path or '') if path == '' or khShell32 == nil then return false end local ok, result = pcall(khShell32.ShellExecuteA, nil, 'open', path, nil, nil, 0) if not ok or result == nil then return false end return tonumber(ffi.cast('intptr_t', result)) > 32 end function khCollectKnownNakhodkaFiles() local base = 'moonloader' if type(getWorkingDirectory) == 'function' then local ok, dir = pcall(getWorkingDirectory) if ok and dir ~= nil and tostring(dir) ~= '' then base = tostring(dir) end end local files = { base .. '\\Nakhodka.lua', base .. '\\Nakhodka' .. 'Legal.lua', base .. '\\config\\nakhodka.ini', base .. '\\config\\nakhodka_drop_stats.txt', base .. '\\config\\nakhodka_prices.txt', base .. '\\config\\nakhodka_avg_prices.json', base .. '\\config\\nakhodka_manifest.txt', base .. '\\config\\nakhodka_phrases.json', base .. '\\config\\nakhodka_companions.json', base .. '\\config\\kladhelper_phrases.json', base .. '\\resource\\nakhodka.cashe', base .. '\\resource\\nakhodka_tyan.png', base .. '\\resource\\nakhodka_marat_crop.png', base .. '\\resource\\nakhodka_map.png', base .. '\\resource\\nakhodka_vc.png', base .. '\\lib\\notify.lua' } if type(nkBootSelfPath) == 'function' then local ok, selfPath = pcall(nkBootSelfPath) if ok and selfPath ~= nil and tostring(selfPath) ~= '' then files[#files + 1] = tostring(selfPath) end end return files, base end function khDeleteKnownNakhodkaFiles(skipSelf) local files = khCollectKnownNakhodkaFiles() local selfPath = nil if type(nkBootSelfPath) == 'function' then local ok, path = pcall(nkBootSelfPath); if ok and path ~= nil then selfPath = tostring(path) end end for _, path in ipairs(files) do if not skipSelf or selfPath == nil or tostring(path):lower() ~= selfPath:lower() then pcall(os.remove, path) end end end function khRandomTempSuffix() local ok, crypto = pcall(require, 'crypto_lua') if not ok or type(crypto) ~= 'table' or type(crypto.salsa20_generate_key) ~= 'function' then return nil end local created, key = pcall(crypto.salsa20_generate_key) if not created or type(key) ~= 'string' or #key < 16 then return nil end local digest = nkBootSha256Data(key) if type(digest) ~= 'string' or #digest < 24 then return nil end return digest:sub(1, 24):lower() end function khScheduleNakhodkaUninstall() local files, base = khCollectKnownNakhodkaFiles() local temp = os.getenv('TEMP') or os.getenv('TMP') or base or '.' local suffix = khRandomTempSuffix() if suffix == nil then return false end local batPath = tostring(temp) .. NK_BOOT_SEP .. 'nakhodka_uninstall_' .. suffix .. '.bat' local file = io.open(batPath, 'w') if not file then return false end file:write('@echo off\r\n') file:write('timeout /t 2 /nobreak >nul\r\n') if base ~= nil and tostring(base) ~= '' then file:write('del /f /q "' .. tostring(base):gsub('"', '') .. '\\Nakhodka*.lua" >nul 2>nul\r\n') end for _, path in ipairs(files) do file:write('del /f /q "' .. tostring(path):gsub('"', '') .. '" >nul 2>nul\r\n') end file:write('del /f /q "%~f0" >nul 2>nul\r\n') file:close() local launched = khRunHiddenFile(batPath) if not launched then pcall(os.remove, batPath) end return launched end function khLeaveTeamBeforeShutdown() if type(khTeamHasTeam) ~= 'function' or not khTeamHasTeam() then return true end local token = khTeam and tostring(khTeam.token or '') or '' local sent = false if token ~= '' and type(khTeamJsonEncode) == 'function' and type(khAsyncHttpRequest) == 'function' then local encoded = khTeamJsonEncode({token = token, packets = {{t = 'leave'}}}) if encoded then local finished, succeeded = false, false local launched = pcall(khAsyncHttpRequest, 'POST', tostring(khTeam.apiUrl or 'https://nakhodka.fun/team/poll'), { data = encoded, timeout = 1, kh_thread_timeout = 2, headers = { ['Content-Type'] = 'application/json', ['Accept'] = 'application/json', ['Cache-Control'] = 'no-store' } }, function(response) local status = tonumber(response and (response.status_code or response.status)) succeeded = status == nil or (status >= 200 and status < 300) finished = true end, function() finished = true end) if launched then local deadline = os.clock() + 2.2 while not finished and os.clock() < deadline do wait(0) end sent = finished and succeeded end end end khTeamMembers = {} khTeamInvites = {} khTeamRoster = {} if type(khTeamClearOwnSos) == 'function' then pcall(khTeamClearOwnSos) end if type(khClearTeamMapArtifacts) == 'function' then pcall(khClearTeamMapArtifacts) end return sent end function khConfirmUninstallNakhodka() if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() nakhodkaNotify('Удаляю Nakhodka и файлы. После этого перезапусти игру.', -1, 'warning', 5) if type(khTeamHasTeam) == 'function' and khTeamHasTeam() then nakhodkaNotify('Выхожу из команды перед удалением...', -1, 'info', 2) khLeaveTeamBeforeShutdown() end wait(300) khScheduleNakhodkaUninstall() wait(150) if type(thisScript) == 'function' then local selfScript = thisScript() if selfScript ~= nil then pcall(function() selfScript:unload() end) end end end) else khScheduleNakhodkaUninstall() nakhodkaNotify('Удаление запланировано. Перезапусти игру.', -1, 'warning', 5) end end function khUnloadNakhodka(source) if khUnloadBusy then return false end khUnloadBusy = true khUnloadKeyWaiting = false if khMenuState ~= nil then khMenuState[0] = false end if type(khTeamMapClose) == 'function' then pcall(khTeamMapClose) end nakhodkaNotify('Выгружаю Nakhodka...', -1, 'info', 2) local function performUnload() local unloaded = false if type(thisScript) == 'function' then local selfScript = thisScript() if selfScript ~= nil then unloaded = pcall(function() selfScript:unload() end) end end if not unloaded then khUnloadBusy = false nakhodkaNotify('Не удалось выгрузить Nakhodka.', -1, 'error', 3) end end if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() wait(120) performUnload() end) else performUnload() end return true end function khUnloadHandleHotkey() if khUnloadKeyWaiting then local done, key = khTeamMapCapturePressedKey() if done then khUnloadKeyWaiting = false if key ~= nil then khUnloadKey = key khSaveMainSettings() nakhodkaNotify('Клавиша выгрузки: ' .. khTeamMapKeyName(key), -1, 'success', 2) else nakhodkaNotify('Выбор клавиши выгрузки отменён.', -1, 'info', 2) end end return end local key = math.floor((tonumber(khUnloadKey) or 0) + 0.5) if key > 0 and khTeamMapWasKeyPressed(key) and not khTeamMapIsBlockedByUi() then khUnloadNakhodka('hotkey') end end khNakhodkaDeferredJobs = khNakhodkaDeferredJobs or {} function khDeferMainJob(job) if type(job) ~= 'function' then return false end table.insert(khNakhodkaDeferredJobs, job) return true end function khRunDeferredJobs(limit) limit = tonumber(limit) or 4 local handled = 0 while handled < limit and #khNakhodkaDeferredJobs > 0 do local job = table.remove(khNakhodkaDeferredJobs, 1) if type(job) == 'function' then pcall(job) end handled = handled + 1 end if type(khProcessAveragePriceCacheApply) == 'function' then khProcessAveragePriceCacheApply(160) end end function khCheckForScriptUpdate(force) if NK_SIGNED_UPDATE_REQUIRED and not nkBootSignatureVerifierAvailable() then if force then nakhodkaNotify('Проверка обновления недоступна: нет модуля подписи.', -1, 'warning', 4) end return end if khUpdateChecking then if force then nakhodkaNotify('Проверка обновления уже идет.', -1, 'info', 2) end return end if type(khAsyncDownloadFile) ~= 'function' then if force then nakhodkaNotify('Автообновление недоступно: нет сетевого модуля.', -1, 'error', 4) end return end khUpdateChecking = true if force then nakhodkaNotify('Проверяю обновление...', -1, 'info', 2) end local urls = NK_UPDATE_SCRIPT_URLS or {} local index = 1 local lastError = 'download' local selfPath = nkBootSelfPath() local localText = nkBootReadAll(selfPath) local tempPath = nkBootJoin(nkBootWorkDir(), 'config/nakhodka_update.download.tmp') local manifestPath = tempPath .. '.manifest.tmp' nkBootEnsureDirFor(tempPath) local function finish(updated, reason) pcall(os.remove, tempPath) pcall(os.remove, manifestPath) khUpdateChecking = false if updated then nakhodkaNotify('Обновление установлено. Перезагружаю скрипт...', -1, 'success', 4) if type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then lua_thread.create(function() wait(1200); khReloadThisScript() end) else khReloadThisScript() end elseif reason == 'same' or reason == 'older' then if force then nakhodkaNotify('У тебя уже свежая версия.', -1, 'success', 3) end elseif force then nakhodkaNotify('Не удалось проверить обновление: ' .. tostring(reason or lastError), -1, 'error', 5) else print('Nakhodka update check failed: ' .. tostring(reason or lastError)) end end local function tryNext() local baseUrl = urls[index] index = index + 1 if baseUrl == nil then finish(false, lastError); return end local sep = tostring(baseUrl):find('?', 1, true) and '&' or '?' local requestUrl = tostring(baseUrl) .. sep .. 't=' .. tostring(os.time()) local okRequest, requestError = pcall(khAsyncDownloadFile, requestUrl, tempPath, function() khDeferMainJob(function() local remoteText = tostring(nkBootReadAll(tempPath) or '') if #remoteText < (NK_UPDATE_MIN_SIZE or 1) or not remoteText:find('NK_SCRIPT_VERSION', 1, true) then lastError = 'bad_file' tryNext() return end local manifestUrl = nkBootBuildManifestUrl(requestUrl) if manifestUrl == nil or not manifestUrl:match('^https://') then lastError = 'manifest_url' tryNext() return end pcall(os.remove, manifestPath) local okManifest, manifestError = pcall(khAsyncDownloadFile, manifestUrl, manifestPath, function() khDeferMainJob(function() local manifestText = nkBootReadAll(manifestPath) if type(manifestText) ~= 'string' then lastError = 'manifest_read' tryNext() return end local manifest = { protocol = nkBootManifestField(manifestText, 'protocol'), version = nkBootManifestNumber(manifestText, 'version'), mode = nkBootManifestField(manifestText, 'mode'), sha256 = nkBootManifestField(manifestText, 'sha256'), signature = nkBootManifestField(manifestText, 'signature') } if manifest.protocol ~= 'nakhodka-build-v1' or type(manifest.version) ~= 'number' or not tostring(manifest.mode or ''):match('^[a-z]+$') or not tostring(manifest.sha256 or ''):match('^[0-9A-Fa-f]+$') or #tostring(manifest.sha256 or '') ~= 64 or tostring(manifest.signature or '') == '' then lastError = 'manifest_invalid' tryNext() return end local signed, signatureError = nkBootVerifySignedBuild(remoteText, manifest) if not signed then finish(false, signatureError or 'signature'); return end local localMode, localBuildSig = nkBootBuildSignature(localText) local remoteMode, remoteBuildSig = nkBootBuildSignature(remoteText) if localBuildSig ~= '' and remoteBuildSig ~= '' and (localMode ~= remoteMode or localBuildSig ~= remoteBuildSig) then finish(false, 'build_mismatch') return end if localText ~= nil and remoteText == localText then finish(false, 'same'); return end local remoteVersion = nkExtractScriptVersion(remoteText) local localVersion = tonumber(NK_SCRIPT_VERSION) or nkExtractScriptVersion(localText) if remoteVersion > 0 and localVersion > 0 and remoteVersion <= localVersion then finish(false, remoteVersion < localVersion and 'older' or 'same') return end nkBootCleanupUpdateBackups() if not nkBootWriteAll(selfPath, remoteText) then finish(false, 'write'); return end finish(true, 'updated') end) end, function(reason) khDeferMainJob(function() lastError = 'manifest_' .. tostring(reason or 'download') tryNext() end) end) if not okManifest then lastError = 'manifest_' .. tostring(manifestError or 'request') tryNext() end return end) end, function(reason) khDeferMainJob(function() lastError = tostring(reason or 'download') tryNext() end) end) if not okRequest then lastError = tostring(requestError or 'request') tryNext() end end tryNext() end local khAdditionalPoints = {} local khAdditionalBlips = {} local khDopActivePointId = nil local khDopActiveCheckpointHandle = nil local khDopExpectUntil = nil local khDopExpectStarted = nil local khLastRaceCheckpoint = nil local khDopCheckpointBeforeDig = nil local khDopDigLocation = nil local khDopDigCapturedAt = 0 local khDopCefDetectedAt = 0 local khDopMarkerRadius = 3.0 local khMainTreasurePoints = {} khMarksRemoteUrl = 'https://nakhodka.fun/files/coords.json' khMarksUpdating = false khMarksAutoLoadRequested = false khMarksLastUpdateText = '' khMarksDeferredJob = nil 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 khGetResourceDir() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local resourceDir = base .. '\\resource' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(resourceDir) then if type(createDirectory) == 'function' then pcall(createDirectory, resourceDir) end end return resourceDir end function khGetResourceDir() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local resourceDir = base .. '\\resource' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(resourceDir) then if type(createDirectory) == 'function' then pcall(createDirectory, resourceDir) end end return resourceDir end function khGetResourceDir() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local resourceDir = base .. '\\resource' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(resourceDir) then if type(createDirectory) == 'function' then pcall(createDirectory, resourceDir) end end return resourceDir end function khGetResourceDir() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local resourceDir = base .. '\\resource' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(resourceDir) then if type(createDirectory) == 'function' then pcall(createDirectory, resourceDir) end end return resourceDir end function khGetResourceDir() local base = '.' if type(getWorkingDirectory) == 'function' then base = getWorkingDirectory() end local resourceDir = base .. '\\resource' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(resourceDir) then if type(createDirectory) == 'function' then pcall(createDirectory, resourceDir) end end return resourceDir end function khGetMainMarksPath() return khGetResourceDir() .. '\\nakhodka.cashe' end function khGetLegacyMainMarksDatPath() return khGetConfigDir() .. '\\nakhodka_marks.dat' end function khGetLegacyMainMarksPath() return khGetConfigDir() .. '\\nakhodka_marks.json' end function khRemoveLegacyMainMarksFiles() local paths = { khGetLegacyMainMarksDatPath(), khGetLegacyMainMarksDatPath() .. '.nkhd2-backup', khGetConfigDir() .. '\\nakhodka_marks.v3.dat', khGetLegacyMainMarksPath(), khGetResourceDir() .. '\\nk_7b41a9.cache' } for _, path in ipairs(paths) do if path ~= nil and tostring(path) ~= '' then pcall(os.remove, path) end end 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', '') local position, length = 1, #text local function skipSpace() while position <= length and text:sub(position, position):match('%s') do position = position + 1 end end local function utf8Char(codepoint) if codepoint <= 0x7F then return string.char(codepoint) end if codepoint <= 0x7FF then return string.char(0xC0 + math.floor(codepoint / 0x40), 0x80 + codepoint % 0x40) end if codepoint <= 0xFFFF then return string.char(0xE0 + math.floor(codepoint / 0x1000), 0x80 + math.floor(codepoint / 0x40) % 0x40, 0x80 + codepoint % 0x40) end return string.char(0xF0 + math.floor(codepoint / 0x40000), 0x80 + math.floor(codepoint / 0x1000) % 0x40, 0x80 + math.floor(codepoint / 0x40) % 0x40, 0x80 + codepoint % 0x40) end local function parseString() if text:sub(position, position) ~= '"' then return nil, false end position = position + 1 local result = {} while position <= length do local char = text:sub(position, position) position = position + 1 if char == '"' then return table.concat(result), true end if char == '\\' then local escaped = text:sub(position, position) position = position + 1 local replacements = {['"'] = '"', ['\\'] = '\\', ['/'] = '/', b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} if escaped == 'u' then local hex = text:sub(position, position + 3) local codepoint = tonumber(hex, 16) if codepoint == nil then return nil, false end result[#result + 1] = utf8Char(codepoint) position = position + 4 elseif replacements[escaped] ~= nil then result[#result + 1] = replacements[escaped] else result[#result + 1] = escaped end else result[#result + 1] = char end end return nil, false end local parseValue local function parseArray() position = position + 1 local result = {} skipSpace() if text:sub(position, position) == ']' then position = position + 1; return result, true end while position <= length do local value, ok = parseValue() if not ok then return nil, false end result[#result + 1] = value skipSpace() local separator = text:sub(position, position) position = position + 1 if separator == ']' then return result, true end if separator ~= ',' then return nil, false end skipSpace() end return nil, false end local function parseObject() position = position + 1 local result = {} skipSpace() if text:sub(position, position) == '}' then position = position + 1; return result, true end while position <= length do local key, keyOk = parseString() if not keyOk then return nil, false end skipSpace() if text:sub(position, position) ~= ':' then return nil, false end position = position + 1 local value, valueOk = parseValue() if not valueOk then return nil, false end result[key] = value skipSpace() local separator = text:sub(position, position) position = position + 1 if separator == '}' then return result, true end if separator ~= ',' then return nil, false end skipSpace() end return nil, false end parseValue = function() skipSpace() local char = text:sub(position, position) if char == '"' then return parseString() end if char == '[' then return parseArray() end if char == '{' then return parseObject() end if text:sub(position, position + 3) == 'true' then position = position + 4; return true, true end if text:sub(position, position + 4) == 'false' then position = position + 5; return false, true end if text:sub(position, position + 3) == 'null' then position = position + 4; return nil, true end local number = text:match('[-+]?%d+%.?%d*[eE]?[-+]?%d*', position) if number == nil or number == '' then return nil, false end position = position + #number return tonumber(number), true end local data, ok = parseValue() skipSpace() if ok and position > length then return data 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 -- Координаты храним и как массив, и как поля x/y/z, id берем из API-ответа. return {x, y, z, x = x, y = y, z = z, id = tonumber(entry.id or entry.ID)} end 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 khMarksSignature() return string.char(0x2D, 0x91, 0x4E, 0xA7, 0x05, 0xC3, 0x6B) end function khMarksPayloadSignature() return string.char(0x71, 0x3A, 0xB5) end function khMarksSecret() local encoded = {0x11, 0xDB, 0x6D, 0xD2, 0x8E, 0xE1, 0x1E, 0x88, 0x0B, 0x2F, 0xB3, 0x21, 0x63, 0xC2, 0x4E, 0xB9, 0xA5, 0x6B, 0x26, 0x89, 0xB5, 0x2E, 0x5E, 0x8C, 0x0C, 0xE3, 0x7B, 0x78, 0xC4, 0x13, 0xAA, 0x10} local out = {} for index, value in ipairs(encoded) do out[index] = string.char(khByteXor(value, (index * 29 + 101) % 256)) end return table.concat(out) end function khMarksLegacyMagic() return string.char(78, 75, 72, 68, 50, 58) end function khMarksLegacyKey() local encoded = {0x0B, 0x09, 0xE0, 0x2B, 0xD4, 0x1E, 0x3D, 0xCB, 0x2B, 0x22, 0xF8, 0xD1, 0x73, 0x5D, 0x5F} local out = {} for index, value in ipairs(encoded) do local mask = (index * 29 + 71) % 256 out[index] = string.char(khByteXor(value, mask)) end return table.concat(out) end function khMarksMask(index, key) key = tostring(key or khMarksLegacyKey()) local keyLen = #key if keyLen <= 0 then key = khMarksLegacyKey(); 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) return khMarksProtectPacked(khMainTreasurePoints or {}) end function khMarksUnprotectLegacy(encoded) encoded = tostring(encoded or '') local magic = khMarksLegacyMagic() if encoded:sub(1, #magic) ~= magic then return nil end encoded = encoded:sub(#magic + 1):gsub('%s+', '') if #encoded % 2 ~= 0 then return nil end local chars = {} local index = 1 local legacyKey = khMarksLegacyKey() 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, legacyKey)) chars[#chars + 1] = string.char(value) index = index + 1 end return table.concat(chars) end function khMarksUnprotect(encoded) return khMarksUnprotectLegacy(encoded) end function khMarksAdler(data) local a, b = 1, 0 data = tostring(data or '') for index = 1, #data do a = (a + (data:byte(index) or 0)) % 65521 b = (b + a) % 65521 end return (b * 65536 + a) % 4294967296 end -- Legacy Adler-32 remains readable only for already installed cache files. -- New cache files use SHA-256 for integrity. The cache encryption below is -- obfuscation, not confidentiality: the client necessarily has its key. function khMarksSha256Raw(data) local digest = nkBootSha256Data(data) if digest == nil then return nil end local out = {} for pos = 1, #digest, 2 do local byte = tonumber(digest:sub(pos, pos + 1), 16) if byte == nil then return nil end out[#out + 1] = string.char(byte) end return table.concat(out) end function khMarksSignatureV3() return khMarksSignature() .. string.char(3) end function khMarksRandomSalt() local ok, crypto = pcall(require, 'crypto_lua') if ok and type(crypto) == 'table' and type(crypto.salsa20_generate_key) == 'function' then local generated, salt = pcall(crypto.salsa20_generate_key) if generated and type(salt) == 'string' and #salt >= 16 then return salt:sub(1, 16) end end return nil end function khMarksU32(data, pos) local b1, b2, b3, b4 = data:byte(pos, pos + 3) if b4 == nil then return nil end return b1 + b2 * 256 + b3 * 65536 + b4 * 16777216 end function khMarksCrypt(data, salt) data = tostring(data or '') salt = tostring(salt or '') if salt == '' then return data end local key = khMarksSecret() local state = {} for index = 0, 255 do state[index] = index end local j = 0 for index = 0, 255 do local kb = key:byte((index % #key) + 1) or 0 local sb = salt:byte((index % #salt) + 1) or 0 j = (j + state[index] + kb + sb + index * 13) % 256 state[index], state[j] = state[j], state[index] end local i = 0 j = 0 for _ = 1, 1024 do i = (i + 1) % 256 j = (j + state[i]) % 256 state[i], state[j] = state[j], state[i] end local out = {} for pos = 1, #data do i = (i + 1) % 256 j = (j + state[i]) % 256 state[i], state[j] = state[j], state[i] local sb = salt:byte(((pos - 1) % #salt) + 1) or 0 local mask = state[(state[i] + state[j] + sb) % 256] out[pos] = string.char(khByteXor(data:byte(pos) or 0, mask)) end return table.concat(out) end function khMarksReadVar(data, pos) local result, shift = 0, 0 while pos <= #data do local byte = data:byte(pos) or 0 pos = pos + 1 result = result + (byte % 128) * (2 ^ shift) if byte < 128 then return result, pos end shift = shift + 7 if shift > 35 then return nil, pos end end return nil, pos end function khMarksUnzigzag(value) value = tonumber(value) or 0 local sign = value % 2 local num = (value - sign) / 2 if sign == 1 then return -num - 1 end return num end function khMarksEncodeVar(value) value = math.max(0, math.floor(tonumber(value) or 0)) local out = {} repeat local byte = value % 128 value = math.floor(value / 128) if value > 0 then byte = byte + 128 end out[#out + 1] = string.char(byte) until value <= 0 return table.concat(out) end function khMarksZigzag(value) value = math.floor(tonumber(value) or 0) if value < 0 then return -value * 2 - 1 end return value * 2 end function khMarksPackPoints(points) local out = {khMarksPayloadSignature(), khMarksEncodeVar(#(points or {}))} local px, py, pz = 0, 0, 0 for _, point in ipairs(points or {}) do local x = math.floor((tonumber(point[1]) or 0) * 100 + 0.5) local y = math.floor((tonumber(point[2]) or 0) * 100 + 0.5) local z = math.floor((tonumber(point[3]) or 0) * 100 + 0.5) out[#out + 1] = khMarksEncodeVar(khMarksZigzag(x - px)) out[#out + 1] = khMarksEncodeVar(khMarksZigzag(y - py)) out[#out + 1] = khMarksEncodeVar(khMarksZigzag(z - pz)) px, py, pz = x, y, z end return table.concat(out) end function khMarksUnpackPoints(payload) payload = tostring(payload or '') local sig = khMarksPayloadSignature() if payload:sub(1, #sig) ~= sig then return nil end local count, pos = khMarksReadVar(payload, #sig + 1) if count == nil then return nil end local points = {} local x, y, z = 0, 0, 0 for _ = 1, count do local dx; dx, pos = khMarksReadVar(payload, pos) local dy; dy, pos = khMarksReadVar(payload, pos) local dz; dz, pos = khMarksReadVar(payload, pos) if dx == nil or dy == nil or dz == nil then return nil end x = x + khMarksUnzigzag(dx) y = y + khMarksUnzigzag(dy) z = z + khMarksUnzigzag(dz) points[#points + 1] = {x / 100, y / 100, z / 100} end return points end function khMarksProtectPacked(points) local payload = khMarksPackPoints(points or {}) local salt = khMarksRandomSalt() local digest = khMarksSha256Raw(payload) if salt == nil or digest == nil then return nil end local encrypted = khMarksCrypt(payload, salt) return khMarksSignatureV3() .. salt .. digest .. encrypted end function khMarksUnprotectPacked(encoded) encoded = tostring(encoded or '') local v3 = khMarksSignatureV3() if encoded:sub(1, #v3) == v3 then local saltStart = #v3 + 1 local salt = encoded:sub(saltStart, saltStart + 15) local digest = encoded:sub(saltStart + 16, saltStart + 47) local encrypted = encoded:sub(saltStart + 48) if #salt ~= 16 or #digest ~= 32 or encrypted == '' then return nil end local payload = khMarksCrypt(encrypted, salt) if khMarksSha256Raw(payload) ~= digest then return nil end return khMarksUnpackPoints(payload) end local sig = khMarksSignature() if encoded:sub(1, #sig) ~= sig then return nil end local saltStart = #sig + 1 local salt = encoded:sub(saltStart, saltStart + 15) local checksum = khMarksU32(encoded, saltStart + 16) local encrypted = encoded:sub(saltStart + 20) if checksum == nil or #salt ~= 16 or encrypted == '' then return nil end local payload = khMarksCrypt(encrypted, salt) if khMarksAdler(payload) ~= checksum then return nil end return khMarksUnpackPoints(payload) end function khBuildMainMarksJson(points) local list = points or khMainTreasurePoints or {} local lines = {'{', ' "version": 2,', ' "marks": ['} for index, point in ipairs(list) do local comma = index < #list 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 packed = khMarksProtectPacked(khMainTreasurePoints or {}) if type(packed) ~= 'string' or packed == '' then return false end local file = io.open(khGetMainMarksPath(), 'wb') if not file then return false end file:write(packed) file:close() return true end function khReadMainMarksPoints() local text = khReadTextFile(khGetMainMarksPath()) if text ~= nil and text ~= '' then local points = khMarksUnprotectPacked(text) if type(points) == 'table' and #points > 0 then return points, false end end local legacyText = khReadTextFile(khGetLegacyMainMarksDatPath()) if legacyText ~= nil and legacyText ~= '' then local plain = khMarksUnprotectLegacy(legacyText) or legacyText local points = khParseMainMarksJson(plain) if type(points) == 'table' and #points > 0 then return points, true end end legacyText = khReadTextFile(khGetLegacyMainMarksPath()) if legacyText ~= nil and legacyText ~= '' then local points = khParseMainMarksJson(legacyText) if type(points) == 'table' and #points > 0 then return points, true end end return nil, false end function khParseMainMarksJson(text) local loaded = {} text = tostring(text or ''):gsub('^\239\187\191', '') local number = '[-+]?%d+%.?%d*' for object in text:gmatch('{(.-)}') do local x = tonumber(object:match('["\']x["\']%s*:%s*(' .. number .. ')') or object:match('["\']X["\']%s*:%s*(' .. number .. ')')) local y = tonumber(object:match('["\']y["\']%s*:%s*(' .. number .. ')') or object:match('["\']Y["\']%s*:%s*(' .. number .. ')')) local z = tonumber(object:match('["\']z["\']%s*:%s*(' .. number .. ')') or object:match('["\']Z["\']%s*:%s*(' .. number .. ')')) if x ~= nil and y ~= nil and z ~= nil and x == x and y == y and z == z then loaded[#loaded + 1] = {x, y, z} end end if #loaded == 0 then for x, y, z in text:gmatch('%[%s*(' .. number .. ')%s*,%s*(' .. number .. ')%s*,%s*(' .. number .. ')%s*%]') do loaded[#loaded + 1] = {tonumber(x), tonumber(y), tonumber(z)} end end return loaded end function khReadMainMarksPlain() local points, legacy = khReadMainMarksPoints() if type(points) ~= 'table' then return nil, legacy end return khBuildMainMarksJson(points), legacy end function khMarksRemoteUpdateUrl() local url = tostring(khMarksRemoteUrl or '') if url == '' then return '' end local sep = url:find('?', 1, true) and '&' or '?' return url .. sep .. 't=' .. tostring(os.time()) end function khSaveMarksUpdateInfo(count) khMarksLastUpdateText = os.date('%d.%m.%Y %H:%M:%S') if mainCfg ~= nil then if mainCfg.Marks == nil then mainCfg.Marks = {} end mainCfg.Marks.lastUpdate = khMarksLastUpdateText mainCfg.Marks.count = tonumber(count) or #(khMainTreasurePoints or {}) pcall(inicfg.save, mainCfg, config_file) end end function khApplyMainMarksPoints(points) if type(points) ~= 'table' or #points == 0 then return false end khMainTreasurePoints = points khMainPointListCache = {} khMainPointListCacheAt = 0 khMainTreasureLastRefresh = 0 pcall(khResetMainTreasureChecked) pcall(khClearMainTreasureMarker) pcall(khClear3DPickups) pcall(khClearMainTreasureBlips) khWriteMainMarksEncrypted() khRemoveLegacyMainMarksFiles() pcall(khRefreshMainTreasureBlips, true) return true end function khMarksHttpGetRaw(url, timeoutSeconds) if not khEffilReady or khEffil == nil or type(khEffil.thread) ~= 'function' then return false, 0, '', 'effil unavailable' end local workerSource = [[ return function(url, timeoutSeconds, workerPackagePath, workerPackageCPath) if type(workerPackagePath) == 'string' and workerPackagePath ~= '' then package.path = workerPackagePath end if type(workerPackageCPath) == 'string' and workerPackageCPath ~= '' then package.cpath = workerPackageCPath end local ok, response = pcall(function() local requests = require 'requests' return requests.request('GET', tostring(url or ''), { timeout = tonumber(timeoutSeconds) or 30, headers = { ['Accept'] = 'application/json', ['Cache-Control'] = 'no-store', ['User-Agent'] = 'Nakhodka' } }) end) if not ok then return false, 0, '', tostring(response) end if type(response) ~= 'table' then return false, 0, '', 'invalid response type: ' .. type(response) end local rawStatus = response.status_code or response.status or response.code local status = tonumber(rawStatus) or tonumber(tostring(rawStatus or ''):match('(%d%d%d)')) or 0 local body = response.text or response.content or response.body or response.data or '' return true, status, tostring(body), '' end ]] local okWorker, worker = pcall(function() return loadstring(workerSource)() end) if not okWorker or type(worker) ~= 'function' then return false, 0, '', 'worker create' end local okThread, thread = pcall(function() return khEffil.thread(worker)(url, timeoutSeconds, tostring((package and package.path) or ''), tostring((package and package.cpath) or '')) end) if not okThread or thread == nil then return false, 0, '', 'worker start' end local deadline = os.clock() + math.max(5, tonumber(timeoutSeconds) or 30) + 3 while os.clock() < deadline do local statusName, statusError = thread:status() if statusName == 'completed' then local okGet, workerOk, status, body, reason = pcall(thread.get, thread) if not okGet then return false, 0, '', tostring(workerOk) end return workerOk == true, tonumber(status) or 0, tostring(body or ''), tostring(reason or '') elseif statusName == 'failed' or statusName == 'canceled' then return false, 0, '', tostring(statusError or statusName) end wait(0) end pcall(function() thread:cancel(0) end) return false, 0, '', 'timeout' end function khUpdateMainMarksFromRemote(showNotify, onComplete) if khMarksUpdating then if showNotify then nakhodkaNotify('Точки уже обновляются.', -1, 'info', 2) end return false end local url = khMarksRemoteUpdateUrl() if url == '' or type(lua_thread) ~= 'table' or type(lua_thread.create) ~= 'function' then if showNotify then nakhodkaNotify('Не удалось обновить точки.', -1, 'error', 3) end return false end khMarksUpdating = true local finished = false local function finish(success, count, reason) if finished then return end finished = true khMarksUpdating = false if success then if showNotify then nakhodkaNotify('Точки обновлены.', -1, 'success', 2) end elseif showNotify then nakhodkaNotify('Не удалось обновить точки: ' .. tostring(reason or 'request'), -1, 'error', 4) end if type(onComplete) == 'function' then khDeferMainJob(function() pcall(onComplete, success, count, reason) end) end end local okStart = pcall(lua_thread.create, function() local ok, status, raw, reason = khMarksHttpGetRaw(url, 30) khDeferMainJob(function() if not ok then finish(false, 0, reason); return end if status < 200 or status >= 300 then finish(false, 0, 'HTTP ' .. tostring(status)); return end local points = khParseMainMarksJson(raw) if type(points) ~= 'table' or #points == 0 then finish(false, 0, 'parse'); return end local appliedOk, applied = pcall(khApplyMainMarksPoints, points) if not appliedOk or not applied then finish(false, 0, 'apply'); return end pcall(khSaveMarksUpdateInfo, #points) finish(true, #points) end) end) if not okStart then finish(false, 0, 'worker start') return false end return true end function khStartMainMarksRemoteUpdate() if khMarksUpdating then nakhodkaNotify('Точки уже обновляются.', -1, 'info', 2) return end khUpdateMainMarksFromRemote(true) end function khEnsureMainMarksAsync() if khMarksAutoLoadRequested or khMarksUpdating then return end khMarksAutoLoadRequested = true if type(khUpdateMainMarksFromRemote) ~= 'function' then khMarksAutoLoadRequested = false return end local started = khUpdateMainMarksFromRemote(false, function(ok) khMarksAutoLoadRequested = false if ok then pcall(khLoadMainTreasurePoints) pcall(khResetMainTreasureChecked) pcall(khRefreshMainTreasureBlips, true) end end) if not started then khMarksAutoLoadRequested = false end end function khLoadMainTreasurePoints() local loaded, legacy = khReadMainMarksPoints() if type(loaded) ~= 'table' or #loaded == 0 then pcall(os.remove, khGetMainMarksPath()) if type(khEnsureMainMarksAsync) == 'function' then khEnsureMainMarksAsync() end return #khMainTreasurePoints end khMainTreasurePoints = loaded khMainPointListCache = {} khMainPointListCacheAt = 0 if legacy then khWriteMainMarksEncrypted() end khRemoveLegacyMainMarksFiles() return #loaded end local khMainTreasureBlips = {} local khMainTreasureChecked = {} local khMainTreasureLastRefresh = 0 khMainTreasureFastCheckAt = 0 local khLastDopExpireCheck = 0 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 function khForgetBlip(blip) if type(forgetBlip) == 'function' then pcall(forgetBlip, blip) end end 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 function khRemoveBlip(blip) if blip and type(removeBlip) == 'function' then pcall(removeBlip, blip) end end 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 function khClearAdditionalBlips() for _, blip in pairs(khAdditionalBlips) do if khBlipExists(blip) then khForgetBlip(blip) khRemoveBlip(blip) end end khAdditionalBlips = {} end function khRefreshAdditionalBlips() khClearAdditionalBlips() if not khFeatureEnabled('dopki') or 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 function khAddAdditionalPoint(x, y, z) if not khFeatureEnabled('dopki') then return false end 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 function khFindDopIndexById(id) for index, point in ipairs(khAdditionalPoints) do if point.id == id then return index end end return nil end function khClearActiveDopMarker() if khDopActiveCheckpointHandle then pcall(deleteCheckpoint, khDopActiveCheckpointHandle) khDopActiveCheckpointHandle = nil end if type(removeWaypoint) == 'function' then pcall(removeWaypoint) end khDopActivePointId = nil end 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 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 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 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 function khDopPlayerIsStopped() if type(getCharSpeed) == 'function' then local ok, speed = pcall(getCharSpeed, PLAYER_PED) if ok and tonumber(speed) ~= nil then return tonumber(speed) <= 0.05 end end return true end function khBuildSortedDopList() local px, py, pz = getCharCoordinates(PLAYER_PED) local entries = {} local byId = {} local signatureParts = {} 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 local id = tostring(point.id or index) local entry = {index = index, distance = dist, id = id} table.insert(entries, entry) byId[id] = entry table.insert(signatureParts, id) end local signature = table.concat(signatureParts, ',') -- Пересортировка только когда игрок стоит или список изменился, -- чтобы строки не прыгали под курсором во время езды. if khDopListOrderIds == nil or signature ~= khDopListOrderSignature or khDopPlayerIsStopped() then table.sort(entries, function(a, b) return a.distance < b.distance end) khDopListOrderIds = {} for _, entry in ipairs(entries) do table.insert(khDopListOrderIds, entry.id) end khDopListOrderSignature = signature return entries end local ordered = {} for _, id in ipairs(khDopListOrderIds) do if byId[id] ~= nil then table.insert(ordered, byId[id]) end end return ordered end 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 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 khDetectActualServerKeyForTreasure() local text = khGetCurrentServerTextForTreasure() local normalizedText = khNormalizeServerForTreasure(text) if normalizedText == '' then return '' 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 '' end function khIsNoTreasureServer() local nowClock = os.clock() if nowClock - (tonumber(khNoTreasureServerCacheAt) or 0) < 1.0 then return khNoTreasureServerCacheValue end khNoTreasureServerCacheAt = nowClock khNoTreasureServerCacheValue = khDetectActualServerKeyForTreasure() == 'vicecity' return khNoTreasureServerCacheValue end function khIsViceCitySleepMode() return khIsNoTreasureServer() end function khTeamCleanText(value, maxLen) local text = tostring(value or ''):gsub('[\r\n\t]', ' '):gsub('^%s+', ''):gsub('%s+$', '') maxLen = tonumber(maxLen) or 64 if #text > maxLen then text = text:sub(1, maxLen) end return text end function khTeamBufferText(buffer, maxLen) if buffer == nil or ffi == nil then return '' end local text = ffi.string(buffer) if text == nil then return '' end local ok, decoded = pcall(function() return u8:decode(text) end) if ok and decoded ~= nil then text = decoded end return khTeamCleanText(text, maxLen or 64) end function khTeamFromUtf8(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 khTeamNickLooksBad(value) local text = khTeamCleanText(value or '', 32) if text == '' then return true end local lower = text:lower() if lower == 'nil' or lower == 'unknown' or lower == 'player' then return true end if text:match('^%d+$') ~= nil and #text >= 4 then return true end return false end function khTeamGetLocalNicknameById(playerId) playerId = tonumber(playerId) if playerId == nil or type(sampGetPlayerNickname) ~= 'function' then return '' end if type(sampIsPlayerConnected) == 'function' then local okConnected, connected = pcall(sampIsPlayerConnected, playerId) if okConnected and connected == false then return '' end end local okNick, nickname = pcall(sampGetPlayerNickname, playerId) if not okNick then return '' end nickname = khTeamCleanText(nickname or '', 32) if khTeamNickLooksBad(nickname) then return '' end return nickname end function khTeamGetOwnNickname() local nick = khTeamCleanText(khGetMyNickname and khGetMyNickname() or '', 32) if khTeamNickLooksBad(nick) then local localId = khGetMyPlayerId and khGetMyPlayerId() or nil local localNick = khTeamGetLocalNicknameById(localId) if localNick ~= '' then nick = localNick end end if khTeamNickLooksBad(nick) then nick = khTeamCleanText(khTeam.lastGoodNick or '', 32) end if khTeamNickLooksBad(nick) then local localId = khGetMyPlayerId and khGetMyPlayerId() or nil nick = localId ~= nil and ('Player_' .. tostring(localId)) or 'Player' end khTeam.lastGoodNick = nick return nick end function khTeamSetBuffer(buffer, value, maxLen) if buffer == nil or ffi == nil then return end maxLen = tonumber(maxLen) or 192 local text = tostring(value or '') local encoded = u8(text) pcall(ffi.fill, buffer, maxLen, 0) if encoded ~= nil and encoded ~= '' then pcall(ffi.copy, buffer, tostring(encoded):sub(1, math.max(0, maxLen - 1))) end end function khTeamRefreshLocalIdCache(force) local nowClock = os.clock() if not force and nowClock - (tonumber(khTeamLocalIdCacheAt) or 0) < 1.0 then return end khTeamLocalIdCacheAt = nowClock khTeamLocalIdCache = {} if type(sampGetPlayerNickname) ~= 'function' then return end for playerId = 0, 1004 do local connected = true if type(sampIsPlayerConnected) == 'function' then local okConnected, isConnected = pcall(sampIsPlayerConnected, playerId) connected = (not okConnected) or isConnected ~= false end if connected then local okNick, playerNick = pcall(sampGetPlayerNickname, playerId) local norm = okNick and khTeamNormalizePlayerName(playerNick or '') or '' if norm ~= '' and norm ~= 'nil' then khTeamLocalIdCache[norm] = playerId end end end end function khTeamFindLocalPlayerIdByNick(nick) local wanted = khTeamNormalizePlayerName(nick or '') if wanted == '' then return nil end khTeamRefreshLocalIdCache(false) return khTeamLocalIdCache[wanted] end function khTeamDisplayPlayerId(entry) if type(entry) ~= 'table' then return nil end local id = khTeamFindLocalPlayerIdByNick(entry.nick or entry.nickname or entry.name or '') if id ~= nil then return id end local raw = tonumber(entry.playerId or entry.sampId or entry.id) if raw ~= nil and raw > 0 then return raw end return nil end function khTeamDisplayNick(entry, maxLen) if type(entry) ~= 'table' then return 'Player' end local nick = khTeamCleanText(khTeamFromUtf8(entry.nick or entry.nickname or entry.name or entry.player or ''), maxLen or 32) if khTeamNickLooksBad(nick) then local playerId = khTeamDisplayPlayerId(entry) nick = playerId ~= nil and ('Player #' .. tostring(playerId)) or 'Player' end return khTeamCleanText(nick, maxLen or 32) end function khTeamResolvePlayerTarget(value) local raw = khTeamCleanText(value, 32) if raw == '' then return '', nil, '' end local idText = raw:match('^#?(%d+)$') or raw:match('^%[(%d+)%]$') if idText == nil then return raw, nil, '' end local playerId = tonumber(idText) if playerId == nil or playerId < 0 or playerId > 1004 then return '', nil, idText end if type(sampIsPlayerConnected) == 'function' then local connectedOk, connected = pcall(sampIsPlayerConnected, playerId) if connectedOk and connected == false then return '', nil, idText end end if type(sampGetPlayerNickname) ~= 'function' then return '', nil, idText end local nickOk, nickname = pcall(sampGetPlayerNickname, playerId) if nickOk then local target = khTeamCleanText(nickname, 32) if target ~= '' and target:lower() ~= 'nil' then return target, playerId, idText end end return '', nil, idText end function khTeamNormalizePlayerName(value) return khTeamCleanText(khTeamFromUtf8(value or ''), 32):lower() end function khTeamRosterIsMe(entry) if type(entry) ~= 'table' then return false end local token = khTeamCleanText(entry.token or '', 96) if token ~= '' and token == khTeam.token then return true end local myId = khGetMyPlayerId() local playerId = tonumber(entry.playerId or entry.id or entry.sampId) if myId ~= nil and playerId ~= nil and myId == playerId then return true end local myNick = khTeamNormalizePlayerName(khGetMyNickname and khGetMyNickname() or '') local nick = khTeamNormalizePlayerName(entry.nick or entry.nickname or entry.name or '') return myNick ~= '' and nick ~= '' and myNick == nick end function khTeamRosterAcceptsServer(entry) if type(entry) ~= 'table' then return false end local serverKey = khTeamCleanText(entry.serverKey or entry.room or entry.server or '', 64) if serverKey == '' then return true end if khTeam.serverKey == nil or khTeam.serverKey == '' or khTeam.serverKey == 'unknown' then return true end -- Vice City - общий хаб: туда заходят игроки со всех серверов, -- но в одной комнате должны быть те, кто реально выбрал/определился как Vice City. if khTeam.serverKey == 'vicecity' then return serverKey == 'vicecity' end return serverKey == khTeam.serverKey end function khTeamRosterKey(entry) if type(entry) ~= 'table' then return nil end local token = khTeamCleanText(entry.token or entry.clientToken or entry.uuid or '', 96) if token ~= '' then return 't:' .. token end local playerId = tonumber(entry.playerId or entry.id or entry.sampId) if playerId ~= nil then return 'i:' .. tostring(playerId) end local nickRaw = khTeamCleanText(entry.nick or entry.nickname or entry.name or '', 32) if not khTeamNickLooksBad(nickRaw) then local nick = khTeamNormalizePlayerName(nickRaw) if nick ~= '' then return 'n:' .. nick end end return nil end function khTeamPruneRoster(now) now = now or os.clock() for key, entry in pairs(khTeamRoster or {}) do local lastSeenAt = tonumber(entry.lastSeenAt or 0) or 0 if lastSeenAt <= 0 or now - lastSeenAt > (khTeamRosterTtl or 20.0) or khTeamRosterIsMe(entry) or not khTeamRosterAcceptsServer(entry) then khTeamRoster[key] = nil end end end function khTeamUpsertRosterPlayer(raw, now) if type(raw) ~= 'table' then return end now = now or os.clock() local nick = khTeamCleanText(khTeamFromUtf8(raw.nick or raw.nickname or raw.name or raw.player or ''), 32) local token = khTeamCleanText(raw.token or raw.clientToken or raw.uuid or '', 96) local playerId = tonumber(raw.playerId or raw.sampId or raw.id) if khTeamNickLooksBad(nick) and token ~= '' and khTeamRoster ~= nil then for _, oldEntry in pairs(khTeamRoster) do if oldEntry.token == token and not khTeamNickLooksBad(oldEntry.nick or '') then nick = oldEntry.nick break end end end if nick == '' and token == '' and playerId == nil then return end local serverKey = khTeamCleanText(raw.serverKey or raw.room or raw.server or khTeam.serverKey or '', 64) local candidate = {token = token, nick = nick, playerId = playerId, serverKey = serverKey} if khTeamRosterIsMe(candidate) or not khTeamRosterAcceptsServer(candidate) then return end local key = khTeamRosterKey(candidate) if key == nil then return end local normalizedNick = khTeamNormalizePlayerName(nick) for oldKey, oldEntry in pairs(khTeamRoster or {}) do if oldKey ~= key then local sameToken = token ~= '' and tostring(oldEntry.token or '') == token local sameNick = normalizedNick ~= '' and khTeamNormalizePlayerName(oldEntry.nick or '') == normalizedNick and khTeamRosterAcceptsServer(oldEntry) if sameToken or sameNick then khTeamRoster[oldKey] = nil end end end local entry = khTeamRoster[key] or {} entry.key = key entry.token = token ~= '' and token or entry.token if not khTeamNickLooksBad(nick) then entry.nick = nick elseif khTeamNickLooksBad(entry.nick or '') then entry.nick = '' end entry.playerId = playerId or entry.playerId entry.serverKey = serverKey ~= '' and serverKey or entry.serverKey or khTeam.serverKey entry.serverLabel = khTeamCleanText(raw.serverLabel or raw.roomLabel or raw.label or khTeam.serverLabel or '', 64) entry.online = raw.online ~= false if raw.visible ~= nil then entry.visible = raw.visible ~= false elseif raw.showNearby ~= nil then entry.visible = raw.showNearby ~= false end entry.x = tonumber(raw.x) or entry.x entry.y = tonumber(raw.y) or entry.y entry.z = tonumber(raw.z) or entry.z entry.teamId = tostring(raw.teamId or entry.teamId or '') local lastSeenAgo = tonumber(raw.lastSeenAgo or raw.seenAgo or raw.ago) if lastSeenAgo ~= nil then entry.lastSeenAt = now - math.max(0, lastSeenAgo) else entry.lastSeenAt = now end khTeamRoster[key] = entry end function khTeamImportRosterList(list, now) if type(list) ~= 'table' then return end now = now or os.clock() khTeamRosterLastSeenAt = now for _, raw in pairs(list) do khTeamUpsertRosterPlayer(raw, now) end khTeamPruneRoster(now) end function khTeamBuildRosterList() local now = os.clock() khTeamPruneRoster(now) local result = {} for key, entry in pairs(khTeamRoster or {}) do if entry.online ~= false and entry.visible ~= false and khTeamRosterAcceptsServer(entry) and not khTeamRosterIsMe(entry) then result[#result + 1] = entry end end table.sort(result, function(a, b) local aid = tonumber(khTeamDisplayPlayerId(a) or 99999) or 99999 local bid = tonumber(khTeamDisplayPlayerId(b) or 99999) or 99999 if aid ~= bid then return aid < bid end return khTeamNormalizePlayerName(a.nick or '') < khTeamNormalizePlayerName(b.nick or '') end) return result end function khTeamFindRosterTarget(target, playerId) local targetNick = khTeamNormalizePlayerName(target or '') local targetId = tonumber(playerId) for _, entry in ipairs(khTeamBuildRosterList()) do local entryId = tonumber(khTeamDisplayPlayerId(entry)) if targetId ~= nil and entryId ~= nil and targetId == entryId then return entry end if targetNick ~= '' and khTeamNormalizePlayerName(entry.nick or '') == targetNick then return entry end end return nil end function khTeamLastSeenText(seconds) seconds = math.max(0, tonumber(seconds) or 0) if seconds < 10 then return 'только что' elseif seconds < 60 then return tostring(math.floor(seconds)) .. ' сек. назад' end local minutes = math.floor(seconds / 60) if minutes < 60 then return tostring(minutes) .. ' мин. назад' end local hours = math.floor(minutes / 60) if hours < 24 then return tostring(hours) .. ' ч назад' end local days = math.floor(hours / 24) if days < 30 then return tostring(days) .. ' д назад' end return tostring(math.floor(days / 30)) .. ' мес. назад' end function khTeamNormalizeServer(value) local text = khTeamCleanText(value, 64):lower() text = text:gsub('%.arizona%-rp%.com.*$', '') text = text:gsub('[%s%-_]+', '') text = text:gsub('[^a-z0-9]', '') return text end function khTeamFindServer(value) local raw = khTeamCleanText(value, 64):lower() local source = tostring(raw or '') local compact = khTeamNormalizeServer(source) if compact == '' then return nil end local numberText = raw:match('^#?(%d+)$') if numberText ~= nil then local serverId = tonumber(numberText) for _, server in ipairs(khArizonaServers) do if tonumber(server.id) == serverId then return server end end return nil end for _, server in ipairs(khArizonaServers) do if compact == server.key then return server end for _, alias in ipairs(server.aliases or {}) do local normalized = khTeamNormalizeServer(alias) if compact == normalized or source:find(tostring(alias):lower(), 1, true) ~= nil then return server end end end return nil end function khTeamDetectServerText() local parts = {} if type(sampGetCurrentServerAddress) == 'function' then local ok, address, port = pcall(sampGetCurrentServerAddress) if ok and address then table.insert(parts, tostring(address)) if port then table.insert(parts, tostring(port)) end end end if type(sampGetCurrentServerName) == 'function' then local ok, name = pcall(sampGetCurrentServerName) if ok and name then table.insert(parts, tostring(name)) end end if type(sampGetServerSettingsPtr) == 'function' then local ok, ptr = pcall(sampGetServerSettingsPtr) if ok and ptr then table.insert(parts, tostring(ptr)) end end return table.concat(parts, ' ') end function khTeamDetectServer() khTeam.manualServer = '' local server = khTeamFindServer(khTeamDetectServerText()) if server then khTeam.serverKey = server.key khTeam.serverLabel = server.label return true end khTeam.serverKey = 'unknown' khTeam.serverLabel = 'Не определен' return false end function khTeamPacketToUtf8(value) if type(value) == 'string' then local ok, encoded = pcall(function() return u8(value) end) if ok and encoded ~= nil then return encoded end return value elseif type(value) == 'table' then local result = {} for k, v in pairs(value) do local outKey = k if type(k) == 'string' then local okKey, encodedKey = pcall(function() return u8(k) end) if okKey and encodedKey ~= nil then outKey = encodedKey end end result[outKey] = khTeamPacketToUtf8(v) end return result end return value end function khTeamJsonEncode(packet) if not khJsonReady or khJson == nil then return nil end local ok, result = pcall(function() return khJson.encode(khTeamPacketToUtf8(packet)) end) if ok then return result end return nil end function khTeamJsonDecode(line) if not khJsonReady or khJson == nil or type(khJson.decode) ~= 'function' or type(line) ~= 'string' then return nil end -- dkjson reports malformed input as nil/position/error. Do not wrap this -- pure-Lua decoder in a C pcall: MoonLoader may try to resume that protected -- frame from another scheduler tick and kill the script with -- "cannot resume non-suspended coroutine". local result = khJson.decode(line, 1, nil) if type(result) == 'table' then return result end return nil end function khTeamSetStatus(status, text) khTeam.status = status or 'offline' text = tostring(text or '') if status == 'online' and text == 'подключено' then text = 'Подключено' end khTeam.statusText = text khTeam.connected = status == 'online' if khTeam.connected then khTeam.lastError = '' khTeam.lastErrorAt = 0 end end function khTeamSetLastError(text) khTeam.lastError = tostring(text or '') khTeam.lastErrorAt = os.clock() end function khTeamClearOwnSos() khTeam.ownSosActive = false khTeam.ownSosUntil = 0 khTeam.ownSosX, khTeam.ownSosY, khTeam.ownSosZ = nil, nil, nil end khTeamSosSeenKeys = khTeamSosSeenKeys or {} khTeamSosLifetimeSeconds = 30.0 function khTeamMemberSosState(member) if type(member) ~= 'table' then return false, 0, nil, nil, nil end local sos = member.sos or member.sosSignal or member.alert if type(sos) == 'table' and sos.active == false then return false, 0, nil, nil, nil end local explicitlyActive = sos == true or type(sos) == 'table' or member.sosActive == true local rawUntil = tonumber(member.sosUntil or member.sos_until or member.sosExpiresAt or member.sos_expires_at or (type(sos) == 'table' and (sos.untilAt or sos.until_at or sos.expiresAt or sos.expires_at))) local lifetime = tonumber(khTeamSosLifetimeSeconds) or 30 local remaining = 0 if rawUntil ~= nil then if rawUntil > 100000000000 then remaining = rawUntil / 1000 - os.time() elseif rawUntil > 1000000000 then remaining = rawUntil - os.time() elseif rawUntil > 0 and rawUntil <= 600 then remaining = rawUntil end if remaining > lifetime then remaining = remaining - math.max(0, 120 - lifetime) end elseif explicitlyActive then remaining = lifetime end if remaining <= 0 then return false, 0, nil, nil, nil end remaining = math.min(lifetime, remaining) -- SOS is a fixed incident point. Never fall back to the member's live -- position here, otherwise the red SOS marker follows the player. local x = tonumber(member.sosX or member.sos_x or (type(sos) == 'table' and sos.x)) local y = tonumber(member.sosY or member.sos_y or (type(sos) == 'table' and sos.y)) local z = tonumber(member.sosZ or member.sos_z or (type(sos) == 'table' and sos.z)) or 0 return x ~= nil and y ~= nil, remaining, x, y, z end function khTeamSosEventKey(identity, x, y) return tostring(khTeam.teamId or '') .. '|' .. tostring(identity or '') .. '|' .. string.format('%.1f|%.1f', tonumber(x) or 0, tonumber(y) or 0) end function khTeamRestoreSosFromState() local best = nil for token, member in pairs(khTeamMembers or {}) do local active, remaining, x, y, z = khTeamMemberSosState(member) if active then local label = khTeamDisplayNick(member, 32) local key = khTeamSosEventKey(label, x, y) if not khTeamSosSeenKeys[key] and (best == nil or remaining > best.remaining) then best = {key=key, remaining=remaining, x=x, y=y, z=z, label=label} end end end if best ~= nil then khTeamSosSeenKeys[best.key] = true khTeamSetTargetMarker(best.x, best.y, best.z, best.label, 'sos', best.remaining) end end function khTeamHasTeam() return tostring(khTeam.teamId or '') ~= '' end function khTeamRequireTeam(actionText) if not khTeamCommandEnabled() then return false end if khTeamHasTeam() then return true end khTeamClearOwnSos() khTeamMembers = {} local text = actionText or 'Сначала вступи в команду.' nakhodkaNotify('Команда: ' .. text, -1, 'error', 4) return false end function khTeamGenerateToken() local nick = khTeamGetOwnNickname and khTeamGetOwnNickname() or 'player' nick = tostring(nick or 'player'):gsub('[^%w_]', '') if nick == '' then nick = 'player' end local ok, crypto = pcall(require, 'crypto_lua') if not ok or type(crypto) ~= 'table' or type(crypto.salsa20_generate_key) ~= 'function' then return nil end local created, key = pcall(crypto.salsa20_generate_key) if not created or type(key) ~= 'string' or #key < 16 then return nil end local digest = nkBootSha256Data(key) if type(digest) ~= 'string' or #digest < 32 then return nil end return nick .. '_' .. digest:sub(1, 32):lower() end function khTeamEnsureToken() if khTeam.token == nil or tostring(khTeam.token) == '' then local token = khTeamGenerateToken() if token == nil then return false end khTeam.token = token if khTeamSaveSettings then khTeamSaveSettings() end end return true end function khTeamQueue(packet) if type(packet) ~= 'table' then return end if khTeamEnabled ~= nil and not khTeamEnabled[0] then return end table.insert(khTeamOutgoing, packet) while #khTeamOutgoing > 200 do table.remove(khTeamOutgoing, 1) end end function khTeamQueueHello() if khTeamEnabled ~= nil and not khTeamEnabled[0] then return end if not khTeamEnsureToken() then return end khTeamDetectServer() local myPlayerId = khGetMyPlayerId() khTeamQueue({ t = 'hello', protocol = 1, token = khTeam.token, nick = khTeamGetOwnNickname and khTeamGetOwnNickname() or 'player', playerId = myPlayerId, id = myPlayerId, serverKey = khTeam.serverKey, serverLabel = khTeam.serverLabel, adminMessageVersion = 1, visible = khTeamShowNearby ~= nil and khTeamShowNearby[0] and true or false, showNearby = khTeamShowNearby ~= nil and khTeamShowNearby[0] and true or false, wantRoster = true }) end khTeamEmojiTokens = {':)', ':(', ':p', ':D', ':O', ':ok:'} function khTeamAppendChatMessage(raw, fromHistory) if type(raw) ~= 'table' then return end local text = khTeamCleanText(khTeamFromUtf8(raw.text or raw.message or ''), 480) if text == '' then return end local from = khTeamCleanText(khTeamFromUtf8(raw.from or raw.nick or raw.name or 'team'), 32) if from == '' then from = 'team' end local msgId = tostring(raw.id or raw.msgId or (tostring(raw.ts or os.time()) .. ':' .. from .. ':' .. text)) if khTeamChatSeen[msgId] then return end khTeamChatSeen[msgId] = true local item = {id = msgId, from = from, text = text, ts = tonumber(raw.ts or raw.createdAt or os.time()) or os.time()} table.insert(khTeamChatLogs, item) while #khTeamChatLogs > 90 do table.remove(khTeamChatLogs, 1) end khTeamChatNeedsScroll = true if not fromHistory and rawSampAddChatMessage ~= nil then local line = string.format('{9D6DFF}[Nakhodka Team]{FFFFFF} %s: %s', from, text) pcall(rawSampAddChatMessage, line, -1) end end function khTeamAppendAdminMessage(raw) if type(raw) ~= 'table' then return end local text = khTeamCleanText(khTeamFromUtf8(raw.message or raw.text or ''), 300) if text == '' then return end local sender = khTeamCleanText(khTeamFromUtf8(raw.displaySender or raw.sender or raw.from or 'Nakhodka'), 32) if sender == '' then sender = 'Nakhodka' end local color = tostring(raw.senderColor or raw.color or 'FF4040'):upper() if not color:match('^[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F]$') then color = 'FF4040' end local messageId = 'admin:' .. tostring(raw.id or raw.messageId or raw.msgId or (tostring(raw.createdAt or os.time()) .. ':' .. sender .. ':' .. text)) if khTeamChatSeen[messageId] then return end khTeamChatSeen[messageId] = true if rawSampAddChatMessage ~= nil then local prefix = raw.showPrefix == true and '{B84DFF}[Nakhodka Team] ' or '' rawSampAddChatMessage(prefix .. '{' .. color .. '}' .. sender .. '{FFFFFF}: ' .. text, -1) end end function khTeamImportChatHistory(list) if type(list) ~= 'table' then return end for _, msg in ipairs(list) do khTeamAppendChatMessage(msg, true) end end function khTeamSendChat(text) if not khTeamRequireTeam('Чат доступен только участникам команды.') then return end text = khTeamCleanText(text or '', 480) if text == '' then nakhodkaNotify('Используй: /kht текст сообщения', -1, 'info', 3) return end khTeamQueue({t = 'team_chat', text = text}) end function khTeamCmdChat(arg) khTeamSendChat(arg or '') end function khTeamRequestRoster(force) if khTeamEnabled ~= nil and not khTeamEnabled[0] then return end local now = os.clock() if not force and now - (khTeamRosterLastRequestAt or 0) < 4.0 then return end khTeamRosterLastRequestAt = now if not khTeamEnsureToken() then return end khTeamDetectServer() local myPlayerId = khGetMyPlayerId() khTeamQueue({ t = 'roster_request', protocol = 1, token = khTeam.token, nick = khTeamGetOwnNickname and khTeamGetOwnNickname() or 'player', playerId = myPlayerId, id = myPlayerId, serverKey = khTeam.serverKey, serverLabel = khTeam.serverLabel, visible = khTeamShowNearby ~= nil and khTeamShowNearby[0] and true or false, showNearby = khTeamShowNearby ~= nil and khTeamShowNearby[0] and true or false }) end local khZoneCopiedPrefix='\xc7\xee\xed\xe0 \xe8\xe3\xf0\xee\xea\xe0 ' local khZoneCopiedSuffix=' \xf1\xea\xee\xef\xe8\xf0\xee\xe2\xe0\xed\xe0 \xe2 \xe1\xf3\xf4\xe5\xf0.' local khZoneExpiredMessage='\xc7\xee\xed\xe0 \xea\xeb\xe0\xe4\xe0 \xf3\xe4\xe0\xeb\xe5\xed\xe0: \xef\xf0\xee\xf8\xeb\xee 60 \xec\xe8\xed\xf3\xf2.' khZoneLifetimeSeconds = 3600 khLastZoneExpireCheck = 0 function khZoneEpoch(value) local stamp=tonumber(value) if stamp and stamp>100000000000 then stamp=math.floor(stamp/1000) end return stamp end function khZoneCreatedAt(zone) if type(zone)~='table' then return nil end local stamp=khZoneEpoch(zone.createdAt or zone.created_at or zone.savedAt or zone.saved_at or zone.timestamp or zone.ts) if stamp then return stamp end local expires=khZoneEpoch(zone.expiresAt or zone.expires_at) if expires then return expires-khZoneLifetimeSeconds end return nil end khLegacyTeamZoneFirstSeen=khLegacyTeamZoneFirstSeen or {} function khZoneIsFresh(zone,legacyKey) if type(zone)~='table' or zone.active==false then return false end local stamp=khZoneCreatedAt(zone) if not stamp and legacyKey~=nil and tostring(legacyKey)~='' then local fingerprint=tostring(legacyKey)..':'..string.format('%.2f:%.2f:%.2f:%.2f',tonumber(zone.left)or 0,tonumber(zone.up)or 0,tonumber(zone.right)or 0,tonumber(zone.down)or 0) stamp=tonumber(khLegacyTeamZoneFirstSeen[fingerprint]) if not stamp then stamp=os.time();khLegacyTeamZoneFirstSeen[fingerprint]=stamp end end if not stamp then return false end local age=os.time()-stamp return age>=-300 and age1 then khPresencePrint(nick..khPresenceNotUses..' (последний раз '..tostring(math.floor(value))..' мин. назад)') else khPresencePrint(nick..khPresenceNotUses) end end,function() khPresencePrint(khPresenceUnavailable) end) if not okStart then khPresencePrint(khPresenceUnavailable) end return okStart end function khPresenceHandleIdCommand(command) local playerId=tonumber(tostring(command or ''):lower():match('^/?id%s+(%d+)%s*$')) if not playerId then return end local now=os.clock() if now-(tonumber(khPresenceProbeAt)or 0)<0.35 then return end khPresenceProbeAt=now local nick=type(khTeamGetLocalNicknameById)=='function' and khTeamGetLocalNicknameById(playerId) or '' khPresencePendingIds[playerId]={at=now,nick=tostring(nick or '')} khPresenceSendHeartbeat(true) end function khPresenceSendRawId(playerId) if type(raknetNewBitStream)~='function' or type(raknetSendRpc)~='function' then return false end local command='/id '..tostring(playerId) local bs=raknetNewBitStream() if bs==nil then return false end raknetBitStreamWriteInt32(bs,#command) raknetBitStreamWriteString(bs,command) raknetSendRpc(50,bs) raknetDeleteBitStream(bs) return true end function khPresenceHandleIdServerMessage(message) message=tostring(message or '') if not message:find('UID:',1,true) or not message:find('packetloss',1,true) then return false end local playerId=tonumber(message:match('%[(%d+)%]')) local uid=message:match('UID:%s*(%d+)') if not playerId or not uid then return false end local ownId=type(khGetMyPlayerId)=='function' and tonumber(khGetMyPlayerId()) or nil if khPresenceSelfProbePending and ownId~=nil and playerId==ownId then khPresenceSelfProbePending=false khPresenceOwnUid=tostring(uid) khPresenceSendHeartbeat(true) return true end local pending=khPresencePendingIds[playerId] if type(pending)~='table' or os.clock()-(tonumber(pending.at)or 0)>8 then return false end khPresencePendingIds[playerId]=nil local nick=tostring(pending.nick or '') if nick=='' and type(khTeamGetLocalNicknameById)=='function' then nick=tostring(khTeamGetLocalNicknameById(playerId)or'') end if nick=='' then nick=tostring(message:match('%]%s*([^|]+)%s*|')or('Player ['..tostring(playerId)..']')):gsub('^%s+',''):gsub('%s+$','') end khPresenceProbeIdentity(playerId,nick,uid) return false end function khPresenceTick(now) if not khIsScriptEnabled() then return end khPresenceSendHeartbeat(false) now=tonumber(now)or os.clock() if tostring(khPresenceOwnUid or '')=='' and not khPresenceSelfProbePending and (tonumber(khPresenceSelfProbeCount)or 0)<3 and now-(tonumber(khPresenceSelfProbeAt)or 0)>=15 then local ownId=type(khGetMyPlayerId)=='function' and tonumber(khGetMyPlayerId()) or nil if ownId~=nil and khPresenceSendRawId(ownId) then khPresenceSelfProbeAt=now khPresenceSelfProbeCount=(tonumber(khPresenceSelfProbeCount)or 0)+1 khPresenceSelfProbePending=true end elseif khPresenceSelfProbePending and now-(tonumber(khPresenceSelfProbeAt)or 0)>8 then khPresenceSelfProbePending=false end end khTeamQueueZoneNow = function() if not khTeamEnabled[0] then return end local zone = khTeamOwnZone or {} if zone.active and khZoneIsFresh(zone) then local createdAt=khZoneCreatedAt(zone) or os.time() khTeamQueue({ t = 'zone', active = true, zone = { left = tonumber(zone.left) or 0, up = tonumber(zone.up) or 0, right = tonumber(zone.right) or 0, down = tonumber(zone.down) or 0, createdAt = createdAt, expiresAt = createdAt + khZoneLifetimeSeconds } }) else khTeamQueue({t = 'zone', active = false}) end end function khTeamQueueIncoming(packet) if type(packet) ~= 'table' then return end table.insert(khTeamIncoming, packet) while #khTeamIncoming > 200 do table.remove(khTeamIncoming, 1) end end function khTeamTakeOutgoing(maxCount) local packets = {} maxCount = tonumber(maxCount) or 24 while #khTeamOutgoing > 0 and #packets < maxCount do table.insert(packets, table.remove(khTeamOutgoing, 1)) end return packets end function khTeamRestoreOutgoing(packets) if type(packets) ~= 'table' then return end for index = #packets, 1, -1 do table.insert(khTeamOutgoing, 1, packets[index]) end while #khTeamOutgoing > 200 do table.remove(khTeamOutgoing, 201) end end function khTeamHttpPostRaw(url, encoded, timeoutSeconds) if not khEffilReady or khEffil == nil or type(khEffil.thread) ~= 'function' then return false, 0, '', 'effil unavailable' end local workerSource = [[ return function(url, encoded, timeoutSeconds, workerPackagePath, workerPackageCPath) if type(workerPackagePath) == 'string' and workerPackagePath ~= '' then package.path = workerPackagePath end if type(workerPackageCPath) == 'string' and workerPackageCPath ~= '' then package.cpath = workerPackageCPath end local ok, response = pcall(function() local requests = require 'requests' return requests.request('POST', tostring(url or ''), { data = tostring(encoded or ''), timeout = tonumber(timeoutSeconds) or 10, headers = { ['Content-Type'] = 'application/json', ['Accept'] = 'application/json', ['Cache-Control'] = 'no-store' } }) end) if not ok then return false, 0, '', tostring(response) end if type(response) ~= 'table' then return false, 0, '', 'invalid response type: ' .. type(response) end local rawStatus = response.status_code or response.status or response.code local status = tonumber(rawStatus) or tonumber(tostring(rawStatus or ''):match('(%d%d%d)')) or 0 local body = response.text or response.content or response.body or response.data or '' return true, status, tostring(body), '' end ]] local okWorker, worker = pcall(function() return loadstring(workerSource)() end) if not okWorker or type(worker) ~= 'function' then return false, 0, '', 'worker create' end local okThread, thread = pcall(function() return khEffil.thread(worker)(url, encoded, timeoutSeconds, tostring((package and package.path) or ''), tostring((package and package.cpath) or '')) end) if not okThread or thread == nil then return false, 0, '', 'worker start' end local deadline = os.clock() + math.max(5, tonumber(timeoutSeconds) or 10) + 3 while os.clock() < deadline do local statusName, statusError = thread:status() if statusName == 'completed' then local okGet, workerOk, status, body, reason = pcall(thread.get, thread) if not okGet then return false, 0, '', tostring(workerOk) end return workerOk == true, tonumber(status) or 0, tostring(body or ''), tostring(reason or '') elseif statusName == 'failed' or statusName == 'canceled' then return false, 0, '', tostring(statusError or statusName) end wait(0) end pcall(function() thread:cancel(0) end) return false, 0, '', 'timeout' end function khTeamPostPoll(packets, requestEpoch) requestEpoch = tonumber(requestEpoch) or 0 local encoded = khTeamJsonEncode({token = khTeam.token, packets = packets}) if not encoded then return false, 'json' end khTeam.httpInFlight = true -- Team polling uses its own scalar-only effil bridge. Returning the whole -- requests response/table through nested Lua threads caused valid HTTP 200 -- replies to be lost even though nginx received every poll. local ok, status, body, reason = khTeamHttpPostRaw( tostring(khTeam.apiUrl or 'https://nakhodka.fun/team/poll'), encoded, 10) khTeam.httpInFlight = false if requestEpoch ~= (tonumber(khTeam.httpReconnectEpoch) or 0) then return true, 'stale' end if not ok then return false, reason ~= '' and reason or 'request' end if status < 200 or status >= 300 then return false, 'HTTP ' .. tostring(status) end if body == '' then return false, 'empty response' end local payload = khTeamJsonDecode(body) if type(payload) ~= 'table' then -- A valid empty poll is enough to keep transport online even if a -- third-party JSON module rejects a large millisecond integer. if body:find('"ok"%s*:%s*true') and body:find('"packets"%s*:%s*%[%s*%]') then return true end return false, 'invalid json' end if payload.ok == false then return false, tostring(payload.error or 'server rejected request') end if type(payload.packets) == 'table' then for _, packet in ipairs(payload.packets) do khTeamQueueIncoming(packet) end end return true end function khTeamNetworkLoop() khTeamNetworkThreadActive = true while true do wait(250) if not khTeamEnabled[0] or not khIsScriptEnabled() then khTeamSetStatus('offline', 'командный режим отключен') wait(1000) elseif not khEffilReady or khEffil == nil then khTeamSetStatus('error', 'HTTP-поток недоступен') wait(3000) elseif not khJsonReady or khJson == nil then khTeamSetStatus('error', 'JSON недоступен') wait(3000) elseif not khTeamDetectServer() or khTeam.serverKey == 'unknown' then khTeamSetStatus('warning', 'сервер Arizona не определен') wait(3000) else local now = os.clock() if now - (khTeam.httpLastHelloAt or 0) >= 10 then khTeamQueueHello() khTeamRequestRoster(true) khTeam.httpLastHelloAt = now end local outgoing = khTeamTakeOutgoing(24) local packets = outgoing if #packets == 0 then packets = {{t = 'ping'}} end local requestEpoch = tonumber(khTeam.httpReconnectEpoch) or 0 local ok, err = khTeamPostPoll(packets, requestEpoch) if requestEpoch ~= (tonumber(khTeam.httpReconnectEpoch) or 0) then wait(50) elseif ok then khTeamSetStatus('online', 'Подключено') wait(250) else khTeamRestoreOutgoing(outgoing) khTeamSetLastError(err or 'request') khTeamSetStatus('error', 'не удалось подключиться') wait(1000) end end end end function khStartTeamClient() if not khTeamEnabled[0] or not khIsScriptEnabled() then khTeamSetStatus('offline', 'командный режим отключен') return end if not khTeamEnsureToken() then khTeamSetStatus('error', 'crypto_lua недоступен') return end khTeamDetectServer() if not khTeamThreadStarted and type(lua_thread) == 'table' and type(lua_thread.create) == 'function' then khTeamThreadStarted = true lua_thread.create(khTeamNetworkLoop) elseif type(lua_thread) ~= 'table' then khTeamSetStatus('error', 'lua_thread недоступен') end end function khTeamArgb(a, r, g, b) local value = (a * 16777216) + (r * 65536) + (g * 256) + b if value > 2147483647 then value = value - 4294967296 end return value end function khTeamClearTargetMarker(showNotify, keepWaypoint) if khTeamTargetCheckpointHandle ~= nil and type(deleteCheckpoint) == 'function' then pcall(deleteCheckpoint, khTeamTargetCheckpointHandle) end khTeamTargetCheckpointHandle = nil if not keepWaypoint and type(removeWaypoint) == 'function' then pcall(removeWaypoint) end if showNotify and khTeamTarget ~= nil then nakhodkaNotify((khTeamTarget.kind == 'sos' and 'SOS-метка закрыта.' or 'GPS-метка закрыта.'), -1, 'success', 2) end khTeamTarget = nil end function khTeamSetTargetMarker(x, y, z, label, kind, ttl) x, y, z = tonumber(x), tonumber(y), tonumber(z) or 0 if not x or not y then return false end khTeamClearTargetMarker(false) if type(placeWaypoint) == 'function' then pcall(placeWaypoint, x, y, z) end if type(createCheckpoint) == 'function' then local radius = kind == 'sos' and 7.0 or 4.5 local ok, checkpoint = pcall(createCheckpoint, 1, x, y, z, 0.0, 0.0, 0.0, radius) if ok and checkpoint ~= nil then khTeamTargetCheckpointHandle = checkpoint end end khTeamTarget = { x = x, y = y, z = z, label = tostring(label or ''), kind = tostring(kind or 'gps'), createdAt = os.clock(), expiresAt = kind == 'sos' and (os.clock() + math.min(tonumber(khTeamSosLifetimeSeconds) or 30, tonumber(ttl) or tonumber(khTeamSosLifetimeSeconds) or 30)) or 0, arriveRadius = kind == 'sos' and 13.0 or 8.0 } return true end function khTeamUpdateTargetMarker() if khTeamTarget == nil then return end if khTeamTarget.kind == 'gps' and type(khRoadGameWaypoint) == 'function' then local waypoint = khRoadGameWaypoint() if waypoint ~= nil then local dx = (tonumber(waypoint.x) or 0) - (tonumber(khTeamTarget.x) or 0) local dy = (tonumber(waypoint.y) or 0) - (tonumber(khTeamTarget.y) or 0) if dx * dx + dy * dy > 100 then khTeamClearTargetMarker(false, true) return end end end if khTeamTarget.kind == 'sos' and tonumber(khTeamTarget.expiresAt or 0) > 0 and os.clock() >= tonumber(khTeamTarget.expiresAt or 0) then khTeamClearTargetMarker(false) return end local x, y, z = getCharCoordinates(PLAYER_PED) if not x or not y then return end local target = khTeamTarget local dist = khDistance(x, y, z or 0, target.x, target.y, target.z or 0) if dist <= (tonumber(target.arriveRadius) or 8.0) then khTeamClearTargetMarker(true) end end function khTeamMapSetCursor(active) active = active and true or false if active then if khTeamMapCursorOwned then return end if type(showCursor) == 'function' then local ok = pcall(showCursor, true, false) if not ok then pcall(showCursor, true) end end khTeamMapCursorOwned = true else if not khTeamMapCursorOwned then return end if type(showCursor) == 'function' then local ok = pcall(showCursor, false, false) if not ok then pcall(showCursor, false) end end khTeamMapCursorOwned = false end end function khTeamMapClose() khTeamMapOpen = false khTeamMapDragging = false khTeamMapSetCursor(false) end function khTeamMapIsBlockedByUi() if type(sampIsDialogActive) == 'function' and sampIsDialogActive() then return true end if type(sampIsScoreboardOpen) == 'function' and sampIsScoreboardOpen() then return true end if type(sampIsChatInputActive) == 'function' and sampIsChatInputActive() then return true end if type(sampIsCursorActive) == 'function' and sampIsCursorActive() and not khTeamMapCursorOwned then return true end return false end function khTeamMapResourceBase() if type(getWorkingDirectory) == 'function' then local ok, dir = pcall(getWorkingDirectory) if ok and dir ~= nil and tostring(dir) ~= '' then return tostring(dir) .. '\\resource' end end return 'moonloader\\resource' end function khTeamMapEnsure(kind) kind = tostring(kind or 'sa') == 'vc' and 'vc' or 'sa' if khTeamMapTextures[kind] ~= nil then khTeamMapTexture = khTeamMapTextures[kind] khTeamMapTextureKind = kind return true end if khTeamMapTextureTriedByKind[kind] then khTeamMapTexture = nil khTeamMapTextureKind = nil return false end khTeamMapTextureTried = true khTeamMapTextureTriedByKind[kind] = true if type(renderLoadTextureFromFile) == 'function' then local resourceBase = khTeamMapResourceBase() local fileName = kind == 'vc' and 'nakhodka_vc.png' or 'nakhodka_map.png' local paths = { resourceBase .. '\\' .. fileName, 'moonloader\\resource\\' .. fileName, 'moonloader/resource/' .. fileName, kind == 'vc' and 'moonloader\\map\\vc.png' or 'moonloader\\map\\map.png', kind == 'vc' and 'moonloader/map/vc.png' or 'moonloader/map/map.png', kind == 'vc' and 'D:\\Arizona Games Launcher\\bin\\arizona\\moonloader\\map\\vc.png' or 'D:\\Arizona Games Launcher\\bin\\arizona\\moonloader\\map\\map.png' } for _, path in ipairs(paths) do if type(doesFileExist) ~= 'function' or doesFileExist(path) then local ok, tex = pcall(renderLoadTextureFromFile, path) if ok and tex ~= nil then khTeamMapTextures[kind] = tex khTeamMapTexture = tex khTeamMapTextureKind = kind break end end end end if type(renderCreateFont) == 'function' then if khTeamMapFont == nil then local ok, font = pcall(renderCreateFont, 'Arial', 9, 5) if ok then khTeamMapFont = font end end if khTeamMapSmallFont == nil then local ok, font = pcall(renderCreateFont, 'Arial', 8, 5) if ok then khTeamMapSmallFont = font end end end return khTeamMapTexture ~= nil and khTeamMapTextureKind == kind end function khTeamMapFrame() local sw, sh = getScreenResolution() sw, sh = tonumber(sw) or 1280, tonumber(sh) or 720 local maxSize = math.floor(math.min(sw - 40, sh - 40, 760)) if maxSize < 260 then maxSize = 260 end local defaultSize = math.floor(math.min(sw * 0.42, sh * 0.62, 560)) if defaultSize < 330 then defaultSize = 330 end if defaultSize > maxSize then defaultSize = maxSize end local size = tonumber(khTeamMapSize) or 0 if size <= 0 then size = defaultSize end size = math.floor(size + 0.5) if size < 240 then size = 240 end if size > maxSize then size = maxSize end khTeamMapSize = size local x = tonumber(khTeamMapPos and khTeamMapPos.x) local y = tonumber(khTeamMapPos and khTeamMapPos.y) if x == nil or x == 0 then x = math.floor(sw * 0.5 - size * 0.5) end if y == nil or y == 0 then y = math.floor(math.max(28, sh * 0.055)) end x = math.floor(x + 0.5) y = math.floor(y + 0.5) if x < 8 then x = 8 end if y < 8 then y = 8 end if x + size > sw - 8 then x = math.max(8, sw - size - 8) end if y + size > sh - 8 then y = math.max(8, sh - size - 8) end if khTeamMapPos ~= nil then khTeamMapPos.x, khTeamMapPos.y = x, y end return x, y, size end function khTeamMapClampCoord(value) value = tonumber(value) or 0 if value > 3000 then return 3000 end if value < -3000 then return -3000 end return value end function khTeamMapWorldToScreen(wx, wy, mx, my, size) wx, wy = khTeamMapClampCoord(wx), khTeamMapClampCoord(wy) local mult = size / 6000 local x = mx + (wx + 3000) * mult local y = my + size - (wy + 3000) * mult return x, y end function khTeamMapTextWidth(font, text) if font ~= nil and type(renderGetFontDrawTextLength) == 'function' then local ok, len = pcall(renderGetFontDrawTextLength, font, tostring(text or '')) if ok and tonumber(len) ~= nil then return tonumber(len) end end return #tostring(text or '') * 6 end function khTeamMapDrawText(text, x, y, color, font) if type(renderFontDrawText) ~= 'function' or font == nil or tostring(text or '') == '' then return end text = tostring(text or '') pcall(renderFontDrawText, font, text, math.floor(x + 1), math.floor(y + 1), khTeamArgb(230, 0, 0, 0)) pcall(renderFontDrawText, font, text, math.floor(x), math.floor(y), color) end function khTeamMapDrawCircle(x, y, radius, fillColor, label, labelColor, sosText) if type(renderDrawPolygon) == 'function' then pcall(renderDrawPolygon, x, y, radius * 2 + 5, radius * 2 + 5, 28, 0, khTeamArgb(235, 8, 10, 14)) pcall(renderDrawPolygon, x, y, radius * 2, radius * 2, 28, 0, fillColor) elseif type(renderDrawBox) == 'function' then pcall(renderDrawBox, x - radius - 2, y - radius - 2, radius * 2 + 4, radius * 2 + 4, khTeamArgb(235, 8, 10, 14)) pcall(renderDrawBox, x - radius, y - radius, radius * 2, radius * 2, fillColor) end if sosText ~= nil and sosText ~= '' then khTeamMapDrawText(sosText, x + radius + 4, y - 15, khTeamArgb(255, 255, 60, 78), khTeamMapSmallFont or khTeamMapFont) end if label ~= nil and label ~= '' then khTeamMapDrawText(label, x - khTeamMapTextWidth(khTeamMapSmallFont or khTeamMapFont, label) * 0.5, y + radius + 1, labelColor, khTeamMapSmallFont or khTeamMapFont) end end function khTeamMapDrawOwnDopki(mx, my, size) if not khFeatureEnabled('dopki') or khIsNoTreasureServer() or type(khAdditionalPoints) ~= 'table' then return end local fill = khTeamArgb(255, 255, 202, 28) local labelColor = khTeamArgb(255, 255, 232, 82) local io = imguiReady and imgui ~= nil and imgui.GetIO ~= nil and imgui.GetIO() or nil local mouse = io and io.MousePos or nil local clicked = io and io.MouseClicked and io.MouseClicked[0] local mouseX = mouse ~= nil and tonumber(mouse.x) or nil local mouseY = mouse ~= nil and tonumber(mouse.y) or nil for _, entry in ipairs(khBuildSortedDopList()) do local point = khAdditionalPoints[entry.index] local x, y = point and tonumber(point.x), point and tonumber(point.y) if x ~= nil and y ~= nil then local sx, sy = khTeamMapWorldToScreen(x, y, mx, my, size) khTeamMapDrawCircle(sx, sy, 4.8, fill, 'Д#' .. tostring(point.id or entry.index), labelColor) if clicked and mouseX ~= nil and mouseY ~= nil then local dx, dy = mouseX - sx, mouseY - sy if dx * dx + dy * dy <= 16 * 16 then khSetDopMarker(entry.index) return end end end end end function khTeamMapDrawRectWorld(left, up, right, down, mx, my, size, color, borderColor, label) left, up, right, down = tonumber(left), tonumber(up), tonumber(right), tonumber(down) if not left or not up or not right or not down then return end local x1, y1 = khTeamMapWorldToScreen(left, up, mx, my, size) local x2, y2 = khTeamMapWorldToScreen(right, down, mx, my, size) if x2 < x1 then x1, x2 = x2, x1 end if y2 < y1 then y1, y2 = y2, y1 end local w, h = math.max(3, x2 - x1), math.max(3, y2 - y1) if type(renderDrawBox) == 'function' then pcall(renderDrawBox, x1, y1, w, h, color) pcall(renderDrawBox, x1 - 1, y1 - 1, w + 2, 2, borderColor) pcall(renderDrawBox, x1 - 1, y2 - 1, w + 2, 2, borderColor) pcall(renderDrawBox, x1 - 1, y1 - 1, 2, h + 2, borderColor) pcall(renderDrawBox, x2 - 1, y1 - 1, 2, h + 2, borderColor) end if label ~= nil and label ~= '' then khTeamMapDrawText(label, x1 + 2, y1 - 14, borderColor, khTeamMapSmallFont or khTeamMapFont) end end function khMapZoneColors(value, opacity) local n=tonumber(value) or 0 if n<0 then n=n+4294967296 end local r=math.floor(n/16777216)%256 local g=math.floor(n/65536)%256 local bl=math.floor(n/256)%256 return khTeamArgb(math.floor(78*opacity),r,g,bl),khTeamArgb(math.floor(180*opacity),r,g,bl) end function khMapGeneratedZone(id) id=tonumber(id) if id==610 or id==1023 then return true end for _,v in pairs(khTeamMemberZones or {}) do if tonumber(v)==id then return true end end for _,v in pairs(khTeamZoneIds or {}) do if tonumber(v)==id then return true end end return false end function khMapDrawServerZones(mx,my,size,opacity) if type(khGetGangZoneMapState)~='function' then return end local zones,hidden=khGetGangZoneMapState() if hidden or type(zones)~='table' then return end for id,z in pairs(zones) do if type(z)=='table' and not khMapGeneratedZone(z.id or id) then local fill,border=khMapZoneColors(z.color,opacity) khTeamMapDrawRectWorld(z.left,z.up,z.right,z.down,mx,my,size,fill,border,'') end end end function khRoadCopy(t,source) if type(t)~='table' then return nil end local x,y,z=tonumber(t.x or t[1]),tonumber(t.y or t[2]),tonumber(t.z or t[3]) or 0 if not x or not y then return nil end return{x=x,y=y,z=z,source=source or t.source or'local'} end function khRoadTarget() if type(getTargetBlipCoordinates)=='function' then local ok,a,c,d,e=pcall(getTargetBlipCoordinates) if ok then if type(a)=='boolean' and a and tonumber(c) and tonumber(d) then return{x=tonumber(c),y=tonumber(d),z=tonumber(e)or 0,source='waypoint'} end if type(a)~='boolean' and tonumber(a) and tonumber(c) then return{x=tonumber(a),y=tonumber(c),z=tonumber(d)or 0,source='waypoint'} end end end if khDopActivePointId and type(khAdditionalPoints)=='table' then for _,q in ipairs(khAdditionalPoints) do if tostring(q.id)==tostring(khDopActivePointId) then return khRoadCopy(q,'dop') end end end return khRoadCopy(activeMarkerCoord,'main') or khRoadCopy(khTeamTarget,'gps') end function khRoadGameWaypoint() if type(getTargetBlipCoordinates)~='function' then return nil end local ok,a,c,d,e=pcall(getTargetBlipCoordinates) if not ok then return nil end if type(a)=='boolean' then if a and tonumber(c) and tonumber(d) then return{x=tonumber(c),y=tonumber(d),z=tonumber(e)or 0,source='waypoint'} end return nil end if tonumber(a) and tonumber(c) then return{x=tonumber(a),y=tonumber(c),z=tonumber(d)or 0,source='waypoint'} end return nil end function khRoadGroundZ(x,y) local names={'getGroundZFor3dCoord','getGroundZFor3DCoord'} for _,name in ipairs(names) do local fn=_G[name] if type(fn)=='function' then local ok,a,c=pcall(fn,x,y,1000.0) if ok then if type(a)=='boolean' and a and tonumber(c) then return tonumber(c) end if type(a)~='boolean' and tonumber(a) then return tonumber(a) end end end end return 0.0 end function khRoadHandleMapRightClick(mx,my,size) if not imguiReady or imgui==nil or imgui.GetIO==nil then return false end local io=imgui.GetIO() if not io or not io.MousePos or not io.MouseClicked or not io.MouseClicked[1] then return false end local mouseX,mouseY=tonumber(io.MousePos.x),tonumber(io.MousePos.y) if not mouseX or not mouseY or mouseXmx+size or mouseYmy+size then return false end local current=khRoadGameWaypoint() if current then local sx,sy=khTeamMapWorldToScreen(current.x,current.y,mx,my,size) local dx,dy=mouseX-sx,mouseY-sy if dx*dx+dy*dy<=18*18 then if type(removeWaypoint)=='function' then pcall(removeWaypoint) end khRoadState={target=nil,path={},job=nil,origin=nil,status='idle'} return true end end local wx=((mouseX-mx)/size)*6000-3000 local wy=((my+size-mouseY)/size)*6000-3000 wx=math.max(-3000,math.min(3000,wx));wy=math.max(-3000,math.min(3000,wy)) local wz=khRoadGroundZ(wx,wy) if type(removeWaypoint)=='function' then pcall(removeWaypoint) end if type(placeWaypoint)=='function' then local ok=pcall(placeWaypoint,wx,wy,wz) if ok then khRoadState.target={x=wx,y=wy,z=wz,source='waypoint'};khRoadState.origin=nil;khRoadState.path={};khRoadState.job=nil;return true end end return false end khRoadArrivalCheckAt=0 function khRoadNearPoint(a,b,tolerance) if type(a)~='table' or type(b)~='table' then return false end local ax,ay,bx,by=tonumber(a.x),tonumber(a.y),tonumber(b.x),tonumber(b.y) if not ax or not ay or not bx or not by then return false end local dx,dy=ax-bx,ay-by return dx*dx+dy*dy<=(tonumber(tolerance)or 5)^2 end function khRoadWaypointOwnedByFeature(target) if khRoadNearPoint(target,activeMarkerCoord,6) then return true end if khRoadNearPoint(target,khTeamTarget,6) then return true end if khDopActivePointId~=nil then for _,point in ipairs(khAdditionalPoints or {}) do if tostring(point.id)==tostring(khDopActivePointId) and khRoadNearPoint(target,point,6) then return true end end end return false end function khRoadAutoClearArrivedWaypoint(now) now=tonumber(now)or os.clock() if now-(tonumber(khRoadArrivalCheckAt)or 0)<0.20 then return false end khRoadArrivalCheckAt=now local target=khRoadGameWaypoint() if not target or khRoadWaypointOwnedByFeature(target) then return false end local x,y=getCharCoordinates(PLAYER_PED) if not x or not y then return false end local dx,dy=x-target.x,y-target.y if dx*dx+dy*dy>12*12 then return false end if type(removeWaypoint)=='function' then pcall(removeWaypoint) end khRoadState={target=nil,path={},job=nil,origin=nil,status='idle'} return true end function khRoadChanged(a,b) if not a or not b then return a~=b end local x,y=a.x-b.x,a.y-b.y return x*x+y*y>16 or tostring(a.source)~=tostring(b.source) end function khRoadBegin(px,py,pz,t,keepPath) khRoadState.target=khRoadCopy(t,t.source);khRoadState.origin={x=px,y=py,z=pz or 0};if not keepPath then khRoadState.path={} end;khRoadState.job=nil if khRoad and type(khRoad.begin)=='function' then local ok,j=pcall(khRoad.begin,px,py,pz or 0,t.x,t.y,t.z or 0,{maxVisited=50000}) if ok and type(j)=='table' then khRoadState.job=j;khRoadState.status=j.status or'searching';if j.done then khRoadState.path=j.path or{};khRoadState.job=nil end;return end end khRoadState.status='unavailable' end function khRoadUpdate(px,py,pz) local t=khRoadTarget() if not t then khRoadState={target=nil,path={},job=nil,origin=nil,status='idle'};return end if not px or not py then return end local moved=false if khRoadState.origin then local x,y=px-khRoadState.origin.x,py-khRoadState.origin.y;moved=x*x+y*y>140*140 end local targetChanged=khRoadChanged(khRoadState.target,t) if targetChanged or not khRoadState.origin or moved then khRoadBegin(px,py,pz,t,not targetChanged and khRoadState.origin~=nil) end if khRoadState.job and khRoad and type(khRoad.step)=='function' then local ok,done,path,status=pcall(khRoad.step,khRoadState.job,300) if not ok then khRoadState.job=nil;khRoadState.status='error' else khRoadState.status=status or khRoadState.status;if done then khRoadState.job=nil;if type(path)=='table' and #path>=2 then khRoadState.path=path elseif type(khRoadState.path)~='table' or #khRoadState.path<2 then khRoadState.path={} end end end end end function khMapDrawRoad(mx,my,size,opacity,px,py,now) local t=khRoadState.target;if not t then return end local path=khRoadState.path if type(path)=='table' and #path>=2 and type(renderDrawLine)=='function' and px and py then local nearest,score=1,nil for i,q in ipairs(path) do local x,y=px-(q.x or px),py-(q.y or py);local s=x*x+y*y;if not score or s= 0x30 and code <= 0x39 then return string.char(code) end if code >= 0x41 and code <= 0x5A then return string.char(code) end if code >= 0x70 and code <= 0x7B then return 'F' .. tostring(code - 0x6F) end return tostring(code) end function khTeamMapNativeKeyDown(code) code = tonumber(code) or 0 if khUser32 == nil or (code ~= 0x05 and code ~= 0x06) then return false end local ok, state = pcall(khUser32.GetAsyncKeyState, code) state = ok and tonumber(state) or 0 return state < 0 end function khTeamMapWasKeyPressed(code) code = tonumber(code) or 0 if code <= 0 then return false end local pressed = false if type(wasKeyPressed) == 'function' then local ok, res = pcall(wasKeyPressed, code) pressed = pressed or (ok and res and true or false) end if type(isKeyJustPressed) == 'function' then local ok, res = pcall(isKeyJustPressed, code) pressed = pressed or (ok and res and true or false) end if code == 0x05 or code == 0x06 then local down = khTeamMapNativeKeyDown(code) local wasDown = khTeamMapSideKeyState[code] and true or false khTeamMapSideKeyState[code] = down pressed = pressed or (down and not wasDown) end return pressed end function khTeamMapKeyDown(code) code = tonumber(code) or 0 if code <= 0 then return false end if type(isKeyDown) == 'function' then local ok, res = pcall(isKeyDown, code) if ok and res then return true end end return khTeamMapNativeKeyDown(code) end function khTeamMapCapturePressedKey() if khTeamMapWasKeyPressed(0x1B) then return true, nil end if khTeamMapWasKeyPressed(0x05) then return true, 0x05 end if khTeamMapWasKeyPressed(0x06) then return true, 0x06 end for code = 7, 254 do if code ~= 0x1B and khTeamMapWasKeyPressed(code) then return true, code end end return false, nil end function khTeamMapHandleHotkey(now) if khTeamMapKeyWaiting then local done, key = khTeamMapCapturePressedKey() if done then khTeamMapKeyWaiting = false if key ~= nil then khTeamMapKey = key khTeamSaveSettings() nakhodkaNotify('Клавиша мини-карты: ' .. khTeamMapKeyName(key), -1, 'success', 2) else nakhodkaNotify('Выбор клавиши отменен.', -1, 'info', 2) end end return end if khTeamMapEnabled == nil or not khTeamMapEnabled[0] or not khFeatureEnabled('teamMap') then khTeamMapClose(); return end local key = khTeamMapKey or 0x47 if khTeamMapHoldMode ~= nil and khTeamMapHoldMode[0] then if khTeamMapKeyDown(key) and not khTeamMapIsBlockedByUi() then khTeamMapOpen = true khTeamMapSetCursor(true) else khTeamMapClose() end return end if khTeamMapWasKeyPressed(key) and not khTeamMapIsBlockedByUi() then khTeamMapOpen = not khTeamMapOpen if khTeamMapOpen then khTeamMapSetCursor(true) else khTeamMapClose() end end end function khTeamMapHandleMouse(mx, my, size) if not imguiReady or imgui == nil or imgui.GetIO == nil then return false end local io = imgui.GetIO() if io == nil or io.MousePos == nil then return false end local mouseX, mouseY = tonumber(io.MousePos.x), tonumber(io.MousePos.y) if mouseX == nil or mouseY == nil then return false end local middleDown = false local middleClicked = false if io.MouseDown ~= nil then middleDown = io.MouseDown[2] and true or false end if io.MouseClicked ~= nil then middleClicked = io.MouseClicked[2] and true or false end if type(isKeyDown) == 'function' then local ok, res = pcall(isKeyDown, 0x04); middleDown = middleDown or (ok and res) end if type(wasKeyPressed) == 'function' then local ok, res = pcall(wasKeyPressed, 0x04); middleClicked = middleClicked or (ok and res) end local inside = mouseX >= mx - 8 and mouseX <= mx + size + 8 and mouseY >= my - 8 and mouseY <= my + size + 8 if middleClicked and inside then khTeamMapDragging = true khTeamMapDragOffset.x = mouseX - mx khTeamMapDragOffset.y = mouseY - my end if not khTeamMapDragging then return false end if middleDown then khTeamMapPos.x = mouseX - (tonumber(khTeamMapDragOffset.x) or 0) khTeamMapPos.y = mouseY - (tonumber(khTeamMapDragOffset.y) or 0) local wheel = tonumber(io.MouseWheel) or 0 if wheel ~= 0 then khTeamMapSize = (tonumber(khTeamMapSize) or size) + wheel * 34 end return true end khTeamMapDragging = false khTeamMapFrame() khTeamSaveSettings() return false end function khTeamMapHandleZoneRightClick(mx,my,size) if not imguiReady or imgui==nil or imgui.GetIO==nil then return false end local io=imgui.GetIO() if not io or not io.MousePos or not io.MouseClicked or not io.MouseClicked[1] then return false end local mouseX,mouseY=tonumber(io.MousePos.x),tonumber(io.MousePos.y) if not mouseX or not mouseY or mouseXmx+size or mouseYmy+size then return false end local wx=((mouseX-mx)/size)*6000-3000 local wy=((my+size-mouseY)/size)*6000-3000 local bestMember,bestZone,bestArea=nil,nil,nil for token,member in pairs(khTeamMembers or {}) do local zone=type(member.zone)=='table' and member.zone or nil if member.online~=false and zone and khTeamZoneIsFresh(member,token) then local l,r=tonumber(zone.left),tonumber(zone.right) local u,d=tonumber(zone.up),tonumber(zone.down) if l and r and u and d then local minX,maxX=math.min(l,r),math.max(l,r) local minY,maxY=math.min(u,d),math.max(u,d) if wx>=minX and wx<=maxX and wy>=minY and wy<=maxY then local area=math.abs((maxX-minX)*(maxY-minY)) if not bestArea or area (now or os.clock()) and khTeam.ownSosX ~= nil and khTeam.ownSosY ~= nil then local sx, sy = khTeamMapWorldToScreen(khTeam.ownSosX, khTeam.ownSosY, mx, my, size) khTeamMapDrawCircle(sx, sy, 7.0, red, khFeatureEnabled('teamMapNames') and (khGetMyNickname() or 'me') or '', red, 'SOS') end if type(khSpawnRenderTeamMapAll) == 'function' then khSpawnRenderTeamMapAll(mx, my, size) end if type(khSpawnRenderTeamMapNearest) == 'function' then khSpawnRenderTeamMapNearest(mx, my, size) end end function khClearTeamMapArtifacts() for token, blip in pairs(khTeamMemberBlips) do khRemoveBlip(blip) khTeamMemberBlips[token] = nil end if type(removeGangZone) == 'function' then for token, zoneId in pairs(khTeamMemberZones) do pcall(removeGangZone, zoneId) khTeamMemberZones[token] = nil end end khTeamClearTargetMarker(false) end function khTeamGetZoneId(token) if khTeamZoneIds[token] then return khTeamZoneIds[token] end local count = 0 for _ in pairs(khTeamZoneIds) do count = count + 1 end local zoneId = 620 + count khTeamZoneIds[token] = zoneId return zoneId end function khTeamRefreshMapArtifacts() local seen = {} local orange = khTeamArgb(120, 255, 150, 0) local allowZones = khFeatureEnabled('teamWorldZones') and not khIsNoTreasureServer() for token, member in pairs(khTeamMembers) do seen[token] = true local online = member.online ~= false local x, y, z = tonumber(member.x), tonumber(member.y), tonumber(member.z) or 0 if online and x and y then if khTeamMemberBlips[token] then khRemoveBlip(khTeamMemberBlips[token]) khTeamMemberBlips[token] = nil end local blip = khAddSpriteBlipCompat(x, y, z, 19) if blip then khRememberBlip(blip) khTeamMemberBlips[token] = blip end elseif khTeamMemberBlips[token] then khRemoveBlip(khTeamMemberBlips[token]) khTeamMemberBlips[token] = nil end local zone = type(member.zone) == 'table' and member.zone or nil if allowZones and online and zone and khTeamZoneIsFresh(member,token) and type(addGangZone) == 'function' then local zid = khTeamGetZoneId(token) if khTeamMemberZones[token] then pcall(removeGangZone, khTeamMemberZones[token]) end khTeamMemberZones[token] = zid pcall(addGangZone, zid, tonumber(zone.left) or 0, tonumber(zone.up) or 0, tonumber(zone.right) or 0, tonumber(zone.down) or 0, orange) elseif khTeamMemberZones[token] then pcall(removeGangZone, khTeamMemberZones[token]) khTeamMemberZones[token] = nil end end for token, blip in pairs(khTeamMemberBlips) do if not seen[token] then khRemoveBlip(blip) khTeamMemberBlips[token] = nil end end for token, zoneId in pairs(khTeamMemberZones) do if not seen[token] then pcall(removeGangZone, zoneId) khTeamMemberZones[token] = nil end end end function khTeamHandlePacket(packet) local t = tostring(packet.t or '') if t == 'hello' then khTeamSetStatus('online', 'Подключено') if type(packet.players) == 'table' then khTeamImportRosterList(packet.players, os.clock()) end elseif t == 'notice' then nakhodkaNotify(khTeamFromUtf8(packet.text or 'Командное уведомление'), -1, tostring(packet.level or 'info'), 3) elseif t == 'error' then local errText = khTeamFromUtf8(packet.text or packet.code or 'Ошибка') local errCode = tostring(packet.code or '') if errCode == 'no_team' or tostring(errText):find('не состоишь в команде', 1, true) then khTeam.teamId = '' khTeamMembers = {} khTeamClearOwnSos() khClearTeamMapArtifacts() end if tostring(errText):find('не состоишь в команде', 1, true) and next(khTeamMembers) ~= nil then khTeam.lastError = '' khTeam.lastErrorAt = 0 return end for _, invite in pairs(khTeamInvites) do invite.acceptPendingAt = nil end khTeamSetLastError(errText) nakhodkaNotify('Команда: ' .. khTeam.lastError, -1, 'error', 4) elseif t == 'roster' or t == 'team_roster' or t == 'clients' or t == 'players' or type(packet.players) == 'table' or type(packet.users) == 'table' or type(packet.clients) == 'table' or type(packet.roster) == 'table' then khTeamImportRosterList(packet.players or packet.users or packet.clients or packet.roster or packet.members, os.clock()) elseif t == 'presence' or t == 'client_online' or t == 'player_online' then khTeamUpsertRosterPlayer(packet.player or packet.user or packet.client or packet, os.clock()) elseif t == 'client_offline' or t == 'player_offline' then local key = khTeamRosterKey(packet.player or packet.user or packet.client or packet) if key ~= nil then khTeamRoster[key] = nil end elseif t == 'invite' then local from = khTeamCleanText(khTeamFromUtf8(packet.from or packet.fromNick or packet.nick), 32) if from ~= '' then local nowClock = os.clock() local ttl = tonumber(packet.expiresIn or packet.expires_in or packet.ttl) local absolute = tonumber(packet.expiresAt or packet.expires_at) if absolute ~= nil then if absolute > 100000000000 then absolute = math.floor(absolute / 1000) end ttl = absolute - os.time() end if ttl == nil or ttl < 10 then ttl = 60 end ttl = math.max(30, math.min(300, ttl)) local key = from:lower() local existing = khTeamInvites[key] local isNew = existing == nil or (tonumber(existing.expiresAt) or 0) <= nowClock khTeamInvites[key] = { from = from, expiresAt = math.max(nowClock + ttl, existing and tonumber(existing.expiresAt) or 0), acceptPendingAt = existing and existing.acceptPendingAt or nil, inviteId = packet.inviteId or packet.invite_id or (existing and existing.inviteId or nil) } if isNew then nakhodkaNotify('Приглашение в команду от ' .. from .. '. /khaccept ' .. from, -1, 'info', 6) end end elseif t == 'team_state' then local newTeamId = tostring(packet.teamId or '') if tostring(khTeamChatTeamId or '') ~= newTeamId then khTeamChatTeamId = newTeamId khTeamChatLogs = {} khTeamChatSeen = {} khTeamChatNeedsScroll = true end khTeam.teamId = newTeamId khTeam.leaderToken = tostring(packet.leaderToken or '') khTeam.lastError = '' khTeam.lastErrorAt = 0 if newTeamId ~= '' then khTeamInvites = {} end khTeamMembers = {} if khTeam.teamId == '' then khTeamClearOwnSos() khClearTeamMapArtifacts() end if type(packet.members) == 'table' then for _, member in ipairs(packet.members) do local token = type(member) == 'table' and tostring(member.token or '') or '' if type(member) == 'table' and token ~= '' then if token == khTeam.token then local active, remaining, x, y, z = khTeamMemberSosState(member) if active then khTeam.ownSosActive = true khTeam.ownSosUntil = os.clock() + remaining khTeam.ownSosX, khTeam.ownSosY, khTeam.ownSosZ = x, y, z end else khTeamMembers[token] = member khTeamUpsertRosterPlayer(member, os.clock()) end end end khTeamRestoreSosFromState() end if type(packet.chat) == 'table' then khTeamImportChatHistory(packet.chat) end elseif t == 'admin_message' then khTeamAppendAdminMessage(packet) elseif t == 'team_chat' or t == 'chat' then khTeamAppendChatMessage(packet, false) elseif t == 'team_chat_history' or t == 'chat_history' then khTeamImportChatHistory(packet.messages or packet.chat or packet.items) elseif t == 'sos' then local from = khTeamCleanText(khTeamFromUtf8(packet.from), 32) local x, y, z = tonumber(packet.x), tonumber(packet.y), tonumber(packet.z) or 0 if x and y then if from ~= '' and from ~= (khGetMyNickname and khGetMyNickname() or '') then khTeamSosSeenKeys[khTeamSosEventKey(from, x, y)] = true for _, member in pairs(khTeamMembers or {}) do if khTeamDisplayNick(member, 32) == from then member.sos = {active = true, x = x, y = y, z = z} member.sosUntil = khTeamSosLifetimeSeconds end end khTeamSetTargetMarker(x, y, z, from, 'sos', khTeamSosLifetimeSeconds) nakhodkaNotify('SOS от ' .. from .. '! Едь на красный чекпоинт.', -1, 'error', 7) end end elseif t == 'sos_cancel' or t == 'sos_clear' then local from = khTeamCleanText(khTeamFromUtf8(packet.from), 32) if khTeamTarget ~= nil and khTeamTarget.kind == 'sos' and (from == '' or khTeamTarget.label == from) then khTeamClearTargetMarker(false) nakhodkaNotify((from ~= '' and ('SOS от ' .. from .. ' отменен.') or 'SOS отменен.'), -1, 'info', 3) end end end function khTeamSendPosition(now) if not khTeam.connected then return end if now - (khTeam.lastPosAt or 0) < 0.10 then return end local x, y, z = getCharCoordinates(PLAYER_PED) if not x or not y then return end local moved = true if khTeam.lastX ~= nil then local dx, dy, dz = x - khTeam.lastX, y - khTeam.lastY, (z or 0) - (khTeam.lastZ or 0) moved = (dx * dx + dy * dy + dz * dz) >= 1.0 end if moved or now - (khTeam.lastPosAt or 0) >= 0.75 then khTeam.lastX, khTeam.lastY, khTeam.lastZ = x, y, z or 0 khTeam.lastPosAt = now local myPlayerId = khGetMyPlayerId() khTeamQueue({t = 'pos', x = x, y = y, z = z or 0, angle = 0, playerId = myPlayerId, id = myPlayerId, visible = khTeamShowNearby ~= nil and khTeamShowNearby[0] and true or false, showNearby = khTeamShowNearby ~= nil and khTeamShowNearby[0] and true or false}) end end function khTeamSendZoneIfChanged(now) if not khTeam.connected then return end if now - (khTeam.lastZoneAt or 0) < 1.0 then return end local fingerprint if zoneActive then fingerprint = string.format('1:%.2f:%.2f:%.2f:%.2f', tonumber(left) or 0, tonumber(up) or 0, tonumber(right) or 0, tonumber(down) or 0) else fingerprint = '0' end if fingerprint ~= khTeamLastZoneFingerprint then khTeamLastZoneFingerprint = fingerprint khTeam.lastZoneAt = now khTeamQueueZoneNow() end end function khTeamUpdate(now) if not khTeamEnabled[0] or not khIsScriptEnabled() then khClearTeamMapArtifacts() return end if khTeam.lastErrorAt ~= nil and tonumber(khTeam.lastErrorAt) ~= nil and tonumber(khTeam.lastErrorAt) > 0 and now - tonumber(khTeam.lastErrorAt) > 18.0 then khTeam.lastError = '' khTeam.lastErrorAt = 0 end if khTeam.ownSosActive and tonumber(khTeam.ownSosUntil or 0) > 0 and now >= tonumber(khTeam.ownSosUntil or 0) then if khTeam.connected and khTeamHasTeam() then khTeamQueue({t = 'sos_cancel', reason = 'expired'}) end khTeamClearOwnSos() end if khTeamTarget ~= nil and khTeamTarget.kind == 'sos' and tonumber(khTeamTarget.expiresAt or 0) > 0 and now >= tonumber(khTeamTarget.expiresAt or 0) then khTeamClearTargetMarker(false) end if not khTeamThreadStarted then khStartTeamClient() end if now - (khTeam.lastServerDetectAt or 0) >= 5.0 then khTeam.lastServerDetectAt = now local oldKey = khTeam.serverKey khTeamDetectServer() if oldKey ~= khTeam.serverKey and khTeam.connected then khTeamQueueHello() end end local processed = 0 while #khTeamIncoming > 0 and processed < 25 do local packet = table.remove(khTeamIncoming, 1) khTeamHandlePacket(packet) processed = processed + 1 end khTeamPruneRoster(now) if khTeamRosterVisible and khTeam.connected and now - (khTeamRosterLastRequestAt or 0) >= 5.0 then khTeamRequestRoster(false) end khTeamSendPosition(now) khTeamSendZoneIfChanged(now) khTeamUpdateTargetMarker() if now - (khTeam.lastMarkerRefreshAt or 0) >= 1.0 then khTeam.lastMarkerRefreshAt = now khTeamRefreshMapArtifacts() end end function khTeamCmdOpen() khTargetTab = 3 if khMenuState ~= nil then khMenuState[0] = true end end function khTeamCmdInvite(arg) if not khTeamCommandEnabled() then return end local target, resolvedId, sourceId = khTeamResolvePlayerTarget(arg) if target == '' then if sourceId ~= nil and sourceId ~= '' then nakhodkaNotify('Игрок с ID ' .. tostring(sourceId) .. ' не найден на сервере.', -1, 'error', 3) else nakhodkaNotify('Используй: /khinvite Nick или /khinvite ID', -1, 'info', 3) end return end local rosterEntry = khTeamFindRosterTarget(target, resolvedId) if rosterEntry == nil then khTeamRequestRoster(true) nakhodkaNotify('Игрок ' .. target .. ' не найден среди пользователей Nakhodka на этом сервере. Пусть включит команду или обнови "Люди рядом".', -1, 'error', 5) return end local inviteNick = khTeamCleanText(khTeamFromUtf8(rosterEntry.nick or target), 32) if inviteNick == '' then inviteNick = target end local nowClock = os.clock() local inviteCooldown = 5.0 local inviteLeft = inviteCooldown - (nowClock - (tonumber(khTeam.lastInviteAt) or 0)) if (tonumber(khTeam.lastInviteAt) or 0) > 0 and inviteLeft > 0 then nakhodkaNotify(string.format('Следующее приглашение можно отправить через %.0f сек.', math.ceil(inviteLeft)), -1, 'info', 2) return end local inviteId = tonumber(khTeamDisplayPlayerId(rosterEntry) or resolvedId) khTeam.lastInviteAt = nowClock khTeamQueue({t = 'invite', target = inviteNick, targetToken = rosterEntry.token, targetId = inviteId, targetPlayerId = inviteId}) if inviteId ~= nil then nakhodkaNotify('Приглашение отправляется: ' .. inviteNick .. ' [' .. tostring(inviteId) .. ']', -1, 'info', 2) else nakhodkaNotify('Приглашение отправляется: ' .. inviteNick, -1, 'info', 2) end end function khTeamCmdAccept(arg) if not khTeamCommandEnabled() then return end local from = khTeamCleanText(arg, 32) if from == '' then for _, invite in pairs(khTeamInvites) do from = invite.from break end end if from == '' then nakhodkaNotify('Нет активных приглашений. Используй /khaccept Nick', -1, 'info', 3) return end local key = from:lower() local invite = khTeamInvites[key] local nowClock = os.clock() if invite ~= nil and invite.acceptPendingAt ~= nil and nowClock - invite.acceptPendingAt < 3.0 then nakhodkaNotify('Принятие приглашения уже отправлено.', -1, 'info', 2) return end if invite ~= nil then invite.acceptPendingAt = nowClock invite.expiresAt = math.max(tonumber(invite.expiresAt) or 0, nowClock + 15) end khTeamQueue({t = 'accept', from = from, inviteId = invite and invite.inviteId or nil}) nakhodkaNotify('Принимаю приглашение от ' .. from .. '...', -1, 'info', 2) end function khTeamCmdDeny(arg) if not khTeamCommandEnabled() then return end local from = khTeamCleanText(arg, 32) if from == '' then for _, invite in pairs(khTeamInvites) do from = invite.from break end end if from == '' then nakhodkaNotify('Нет активных приглашений.', -1, 'info', 3) return end khTeamQueue({t = 'deny', from = from}) khTeamInvites[from:lower()] = nil end function khTeamCmdLeave() if not khTeamCommandEnabled() then return end if not khTeamHasTeam() then khTeamClearOwnSos() khTeamMembers = {} khTeamInvites = {} khClearTeamMapArtifacts() nakhodkaNotify('Ты не состоишь в команде.', -1, 'info', 3) return end khTeamQueue({t = 'leave'}) khTeamMembers = {} khTeamClearOwnSos() khClearTeamMapArtifacts() end function khTeamCmdKick(arg) if not khTeamRequireTeam('Кикать можно только находясь в команде.') then return end local target, resolvedId, sourceId = khTeamResolvePlayerTarget(arg) if target == '' then if sourceId ~= nil and sourceId ~= '' then nakhodkaNotify('Игрок с ID ' .. tostring(sourceId) .. ' не найден на сервере.', -1, 'error', 3) else nakhodkaNotify('Используй: /khkick Nick или /khkick ID', -1, 'info', 3) end return end khTeamQueue({t = 'kick', target = target}) if resolvedId ~= nil then nakhodkaNotify('Удаление из команды: ' .. target .. ' [' .. tostring(resolvedId) .. ']', -1, 'info', 2) end end function khTeamCmdServer(arg) khTeam.manualServer = '' khTeamDetectServer() khTeamSaveSettings() nakhodkaNotify('Сервер команды определяется автоматически: ' .. tostring(khTeam.serverLabel or 'не определен'), -1, 'info', 3) if khTeamEnabled ~= nil and khTeamEnabled[0] then khTeamQueueHello() end end function khTeamCmdReconnect() if not khTeamCommandEnabled() then return end local now = os.clock() local lastAt = tonumber(khTeam.lastManualReconnectAt) or 0 if lastAt > 0 and now - lastAt < 2.5 then nakhodkaNotify('Переподключение уже запущено.', -1, 'info', 2) return end khTeam.lastManualReconnectAt = now khTeam.httpReconnectEpoch = (tonumber(khTeam.httpReconnectEpoch) or 0) + 1 khTeamOutgoing = {} khTeamIncoming = {} khTeam.httpLastHelloAt = now khTeamRosterLastRequestAt = 0 khTeamRosterLastSeenAt = 0 khTeam.lastError = '' khTeam.lastErrorAt = 0 khTeamSetStatus('warning', 'Подключение...') khStartTeamClient() khTeamQueueHello() khTeamRequestRoster(true) nakhodkaNotify('Командный модуль переподключается...', -1, 'info', 2) end function khTeamCmdCancelSos() if not khTeamCommandEnabled() then return end if not khTeamHasTeam() then khTeamClearOwnSos() khClearTeamMapArtifacts() nakhodkaNotify('SOS не активен: ты не состоишь в команде.', -1, 'info', 3) return end khTeamQueue({t = 'sos_cancel'}) khTeamClearOwnSos() nakhodkaNotify('SOS отменен.', -1, 'info', 3) end function khTeamCmdSos(arg) if not khTeamRequireTeam('SOS можно отправить только находясь в команде.') then return end local action = khTeamCleanText(arg or '', 16):lower() if action == 'off' or action == 'cancel' or action == 'stop' or action == '0' or action == 'отмена' then khTeamCmdCancelSos() return end local now = os.clock() local cooldown = 15.0 local leftCd = cooldown - (now - (tonumber(khTeam.lastSosAt) or 0)) if leftCd > 0 then nakhodkaNotify(string.format('SOS можно отправить через %.0f сек.', math.ceil(leftCd)), -1, 'info', 2) return end local x, y, z = getCharCoordinates(PLAYER_PED) if not x or not y then nakhodkaNotify('Не удалось получить координаты для SOS.', -1, 'error', 3) return end khTeamQueue({t = 'sos', x = x, y = y, z = z or 0}) khTeam.lastSosAt = now khTeam.ownSosActive = true khTeam.ownSosUntil = now + khTeamSosLifetimeSeconds khTeam.ownSosX, khTeam.ownSosY, khTeam.ownSosZ = x, y, z or 0 nakhodkaNotify('SOS отправлен союзникам.', -1, 'error', 5) end function khNormalizeName(name) return tostring(name or ''):gsub('^%s+', ''):gsub('%s+$', ''):lower() end 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 khMarkDropTreasureContext(seconds) khDropTreasureContextUntil = os.clock() + (tonumber(seconds) or 45.0) end function khIsDropTreasureContextActive() return khPendingDropSession ~= nil or os.clock() <= (tonumber(khDropTreasureContextUntil) or 0) end function khIsOwnTreasureDugMessage(message) local text = clearNotifyText(message) local nick = text:match('Кладоискатель%s+([%w_%.]+)%[%d+%]%s+выкопал%s+клад') local myNickname = khGetMyNickname and khGetMyNickname() or nil return nick ~= nil and myNickname ~= nil and khNormalizeName(nick) == 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 function khCopyDopPosition(point) if type(point) ~= 'table' then return nil end local x = tonumber(point.x or point.X or point[1]) local y = tonumber(point.y or point.Y or point[2]) local z = tonumber(point.z or point.Z or point[3]) if not x or not y or not z then return nil end return {x = x, y = y, z = z, t = tonumber(point.t) or os.clock()} end function khDopDistanceSquared(first, second) if type(first) ~= 'table' or type(second) ~= 'table' then return nil end local dx = (tonumber(first.x) or 0) - (tonumber(second.x) or 0) local dy = (tonumber(first.y) or 0) - (tonumber(second.y) or 0) local dz = (tonumber(first.z) or 0) - (tonumber(second.z) or 0) return dx * dx + dy * dy + dz * dz end function khCaptureDopDigContext(force) local now = os.clock() if not force and now - (tonumber(khDopDigCapturedAt) or 0) < 3.0 then return false end khDopCheckpointBeforeDig = khCopyDopPosition(khLastRaceCheckpoint) local x, y, z = getCharCoordinates(PLAYER_PED) local bestPoint = nil local bestDistance2 = 25.0 if x and y and z then for _, point in ipairs(khAdditionalPoints) do local dx = x - (tonumber(point.x) or 0) local dy = y - (tonumber(point.y) or 0) local dz = z - (tonumber(point.z) or 0) local distance2 = dx * dx + dy * dy + dz * dz if distance2 <= bestDistance2 then bestPoint = point bestDistance2 = distance2 end end end if bestPoint then khDopDigLocation = khCopyDopPosition(bestPoint) elseif x and y and z then khDopDigLocation = {x = x, y = y, z = z, t = now} else khDopDigLocation = nil end khDopDigCapturedAt = now return true end function khDopCheckpointDiffersFromBefore(checkpoint) if khDopCheckpointBeforeDig == nil then return true end local distance2 = khDopDistanceSquared(checkpoint, khDopCheckpointBeforeDig) return distance2 ~= nil and distance2 > 0.25 end function khClearDopCheckpointExpectation() khDopExpectUntil = nil khDopExpectStarted = nil end function khAcceptDopCheckpoint(checkpoint) checkpoint = khCopyDopPosition(checkpoint) if checkpoint == nil or not khDopCheckpointDiffersFromBefore(checkpoint) then return false end khAddAdditionalPoint(checkpoint.x, checkpoint.y, checkpoint.z) khDopCefDetectedAt = os.clock() khClearDopCheckpointExpectation() return true end function khRememberRaceCheckpoint(position) local checkpoint = khCopyDopPosition(position) if checkpoint == nil then return end checkpoint.t = os.clock() khLastRaceCheckpoint = checkpoint if khIsScriptEnabled() and khDopExpectUntil and checkpoint.t <= khDopExpectUntil then khAcceptDopCheckpoint(checkpoint) end end function khStartExpectingDopCheckpoint() khDopExpectStarted = os.clock() khDopExpectUntil = khDopExpectStarted + 2.5 end function khCheckDopExpectation() if not khDopExpectUntil then return end if os.clock() <= khDopExpectUntil then return end khClearDopCheckpointExpectation() end function khCefDopSuccessfulSpaceClick() if not khFeatureEnabled('dopki') or not khIsScriptEnabled() or khIsNoTreasureServer() then return false end if os.clock() - (tonumber(khDopDigCapturedAt) or 0) > 3.0 then khCaptureDopDigContext(true) end khStartExpectingDopCheckpoint() local checkpoint = khLastRaceCheckpoint if checkpoint and checkpoint.t and os.clock() - checkpoint.t <= 1.5 then khAcceptDopCheckpoint(checkpoint) end return true end function khTryRemoveDopNear(x, y, z, silent) x, y, z = tonumber(x), tonumber(y), tonumber(z) if not x or not y or not z then return false end local bestIndex = nil local bestDistance2 = 16.0 for index, point in ipairs(khAdditionalPoints) do local dx = x - (tonumber(point.x) or 0) local dy = y - (tonumber(point.y) or 0) local dz = z - (tonumber(point.z) or 0) local distance2 = dx * dx + dy * dy + dz * dz if distance2 <= bestDistance2 then bestIndex = index bestDistance2 = distance2 end end if bestIndex == nil or not khRemoveAdditionalPoint(bestIndex, true) then return false end 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 function khTryRemoveActiveDopByDig(silent, digLocation) local exact = khCopyDopPosition(digLocation) if exact and khTryRemoveDopNear(exact.x, exact.y, exact.z, silent) then return true end 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 function khHandleDopServerMessage(message, silent) if not khFeatureEnabled('dopki') then return end 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, khDopDigLocation) end if khIsOwnKladDigMessage(message) then khTryRemoveActiveDopByDig(silent, khDopDigLocation) end if khIsOwnDopMessage(message) then if os.clock() - (tonumber(khDopCefDetectedAt) or 0) <= 5.0 then return end khTryRemoveActiveDopByDig(silent, khDopDigLocation) if not silent and khIsScriptEnabled() then khStartExpectingDopCheckpoint() end end end khDropLogs = {} khDropPrices = {} khPriceInputs = {} khAveragePricesByName = nil khAveragePricesLoaded = false khAveragePriceItemCache = {} khAveragePriceWorker = nil khAveragePriceCacheApplying = nil khAveragePriceCacheFile = nil khAveragePriceCacheCount = 0 khAveragePriceStartupRequested = false khAveragePriceApplyDone = nil khAveragePriceApplyMode = nil khDropProfitCacheValue = 0 khDropProfitCacheAt = 0 khDropProfitCacheLogCount = -1 function khInvalidateDropProfitCache() khDropProfitCacheAt = 0 khDropProfitCacheLogCount = -1 end function khInvalidateAveragePriceCache() khAveragePriceItemCache = {} khInvalidateDropProfitCache() end khCurrentDropLogId = nil khLastDropAt = 0 khPendingDropSession = nil khDropTreasureContextUntil = 0 khDropItemNameMap = nil khCefDropPrizeList = {} khCefDropSessionActive = false khCefDropRewardMenuOpen = false khCefDropTitleHasKlad = false khCefDropPendingSelect = nil khCefDropWaitingReopen = false khCefDropExpectCloseByView = false khCefDropExpectCloseDeadline = 0 khCefDropDigSucceeded = false khCefDropLastActionAt = 0 khCefDropCurrentItems = {} khCefDropChestActive = false khCefDropChestGarbageCandidate = false khCefDropChestLastInitAt = 0 khCefDropChestItemMap = {} khCefDropChestPicks = {} khDigCursorLockActive = false khDigCursorLockStartedAt = 0 khDigCursorLockCursorSeenAt = 0 khDigCursorLockWindow = nil khDigCursorLockClipped = false function khReleaseDigCursorClip() if khUser32 ~= nil and khDigCursorLockClipped then pcall(khUser32.ClipCursor, nil) end khDigCursorLockClipped = false end function khStartDigCursorLock() if not khFeatureEnabled('digCursor') or khFixedDigCursorEnabled == nil or not khFixedDigCursorEnabled[0] then khStopDigCursorLock() return false end if khUser32 == nil or khIsNoTreasureServer() then return false end khDigCursorLockActive = true khDigCursorLockStartedAt = os.clock() khDigCursorLockCursorSeenAt = 0 local ok, hwnd = pcall(khUser32.GetForegroundWindow) khDigCursorLockWindow = ok and hwnd or nil return true end function khStopDigCursorLock() khDigCursorLockActive = false khDigCursorLockStartedAt = 0 khDigCursorLockCursorSeenAt = 0 khDigCursorLockWindow = nil khReleaseDigCursorClip() end function khUpdateDigCursorLock(nowClock) if not khFeatureEnabled('digCursor') then khStopDigCursorLock() return end if not khDigCursorLockActive or khUser32 == nil then return end nowClock = tonumber(nowClock) or os.clock() if nowClock - (tonumber(khDigCursorLockStartedAt) or 0) > 45.0 or khCefDropRewardMenuOpen or khCefDropDigSucceeded or khIsNoTreasureServer() then khStopDigCursorLock() return end local cursorActive = false if type(sampIsCursorActive) == 'function' then local ok, active = pcall(sampIsCursorActive) cursorActive = ok and active and true or false end if cursorActive then khDigCursorLockCursorSeenAt = nowClock elseif khDigCursorLockCursorSeenAt > 0 and nowClock - khDigCursorLockCursorSeenAt > 0.35 then khStopDigCursorLock() return end local okForeground, foreground = pcall(khUser32.GetForegroundWindow) if not okForeground or foreground == nil or khDigCursorLockWindow == nil or foreground ~= khDigCursorLockWindow then khReleaseDigCursorClip() return end local rect = ffi.new('NK_RECT[1]') local point = ffi.new('NK_POINT[1]') local okRect, hasRect = pcall(khUser32.GetClientRect, foreground, rect) if okRect and hasRect ~= 0 then point[0].x = math.floor((tonumber(rect[0].right) - tonumber(rect[0].left)) * 0.5) point[0].y = math.floor((tonumber(rect[0].bottom) - tonumber(rect[0].top)) * 0.5) local okPoint, converted = pcall(khUser32.ClientToScreen, foreground, point) if okPoint and converted ~= 0 then local centerX = tonumber(point[0].x) local centerY = tonumber(point[0].y) local clip = ffi.new('NK_RECT[1]') clip[0].left = centerX clip[0].top = centerY clip[0].right = centerX + 1 clip[0].bottom = centerY + 1 pcall(khUser32.SetCursorPos, centerX, centerY) local okClip, clipped = pcall(khUser32.ClipCursor, clip) khDigCursorLockClipped = okClip and clipped ~= 0 return end end if type(getScreenResolution) == 'function' then local okSize, sw, sh = pcall(getScreenResolution) if okSize and sw and sh then local centerX = math.floor(sw * 0.5) local centerY = math.floor(sh * 0.5) local clip = ffi.new('NK_RECT[1]') clip[0].left = centerX clip[0].top = centerY clip[0].right = centerX + 1 clip[0].bottom = centerY + 1 pcall(khUser32.SetCursorPos, centerX, centerY) local okClip, clipped = pcall(khUser32.ClipCursor, clip) khDigCursorLockClipped = okClip and clipped ~= 0 end end end 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 function khEscapeDropText(text) text = tostring(text or '') text = text:gsub(string.char(13), ''):gsub(string.char(10), ' ') text = text:gsub('%%', '%%25'):gsub('|', '%%7C'):gsub(';', '%%3B'):gsub('~', '%%7E') return text end function khUnescapeDropText(text) text = tostring(text or '') text = text:gsub('%%7E', '~'):gsub('%%3B', ';'):gsub('%%7C', '|'):gsub('%%25', '%%') return text end 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 khIsValidUtf8(text) text = tostring(text or '') local index = 1 while index <= #text do local first = text:byte(index) if first <= 127 then index = index + 1 elseif first >= 194 and first <= 223 then local second = text:byte(index + 1) if second == nil or second < 128 or second > 191 then return false end index = index + 2 elseif first >= 224 and first <= 239 then local second = text:byte(index + 1) local third = text:byte(index + 2) if second == nil or third == nil or second < 128 or second > 191 or third < 128 or third > 191 then return false end index = index + 3 elseif first >= 240 and first <= 244 then local second = text:byte(index + 1) local third = text:byte(index + 2) local fourth = text:byte(index + 3) if fourth == nil or third == nil or second == nil or second < 128 or second > 191 or third < 128 or third > 191 or fourth < 128 or fourth > 191 then return false end index = index + 4 else return false end end return true end function khDropDecodeUtf8(text) text = tostring(text or '') if text == '' or u8 == nil or not khIsValidUtf8(text) then return text end local ok, decoded = pcall(function() return u8:decode(text) end) if ok and type(decoded) == 'string' 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 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 khEncodeDropItemList(items) local chunks = {} for _, item in ipairs(items or {}) do local name = khCleanDropItemName(item and item.name or '') if name ~= '' then local count = math.max(1, math.floor((tonumber(item.count) or 1) + 0.5)) table.insert(chunks, khEscapeDropText(name) .. '~' .. tostring(count)) end end return table.concat(chunks, ';') end function khDecodeDropItemList(text) local result = {} for chunk in tostring(text or ''):gmatch('[^;]+') do local name, count = chunk:match('^(.-)~([^~]*)$') name = khCleanDropItemName(khUnescapeDropText(name or '')) if name ~= '' then local itemCount = math.max(1, math.floor((tonumber(count) or 1) + 0.5)) local merged = false for _, item in ipairs(result) do if tostring(item.name or '') == name then item.count = (tonumber(item.count) or 0) + itemCount merged = true break end end if not merged then table.insert(result, {name = name, count = itemCount}) end end end return result end 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 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 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 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 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)) if cleanName ~= '' then table.insert(log.items, {name = cleanName, count = itemCount}) end end end table.insert(khDropLogs, log) end end file:close() khInvalidateDropProfitCache() end function khGetDropLogById(id) for _, log in ipairs(khDropLogs) do if tostring(log.id) == tostring(id) then return log end end return nil end 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 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 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 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 khAveragePriceCachePath() return khGetDropStatsPath():gsub('nakhodka_drop_stats%.txt$', 'nakhodka_avg_prices.cache') end function khEnsureAveragePricesLoaded() if khAveragePricesLoaded then return khAveragePricesByName or {} end khAveragePricesLoaded = true khAveragePricesByName = {} -- The JSON can be several megabytes. Parsing it from the render callback -- freezes the game, so startup only schedules a worker and returns. if not khAveragePriceStartupRequested and type(khStartAveragePriceWorker) == 'function' then khAveragePriceStartupRequested = true khStartAveragePriceWorker('cache', nil, function() end) end return khAveragePricesByName 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, workerPackagePath, workerPackageCPath) if type(workerPackagePath) == 'string' and workerPackagePath ~= '' then package.path = workerPackagePath end if type(workerPackageCPath) == 'string' and workerPackageCPath ~= '' then package.cpath = workerPackageCPath end local function copyArgs(value) local copied = {} if type(value) ~= 'table' and type(value) ~= 'userdata' then return copied end for key, item in pairs(value) do if type(item) == 'table' or type(item) == 'userdata' then local nested = {} for nestedKey, nestedValue in pairs(item) do nested[nestedKey] = nestedValue end copied[key] = nested else copied[key] = item end end return copied end local result, response = pcall(function() local requests = require 'requests' return requests.request(tostring(method or 'GET'), tostring(url or ''), copyArgs(args)) end) if result then if type(response) == 'table' then -- Передаём между effil и MoonLoader только простые значения. -- Таблица заголовков requests может не сериализоваться целиком. local rawStatus = response.status_code or response.status or response.code local status = tonumber(rawStatus) or tonumber(tostring(rawStatus or ''):match('(%d%d%d)')) or 0 local body = response.text or response.content or response.body or response.data or '' return {ok = true, status_code = status, text = tostring(body), error = ''} elseif response == nil then return {ok = false, status_code = 0, text = '', error = 'Empty response'} end return {ok = false, status_code = 0, text = '', error = 'Invalid response type: ' .. type(response)} end return {ok = false, status_code = 0, text = '', error = 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, tostring((package and package.path) or ''), tostring((package and package.cpath) or '')) 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, response = pcall(thread.get, thread) if okGet and type(response) == 'table' then if response.ok == true then resolve(response) else reject(response) end else reject('worker result') 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 khAveragePriceWorkerSource = [[ return function(mode, url, jsonPath, cachePath, workerPackagePath, workerPackageCPath) if type(workerPackagePath) == 'string' and workerPackagePath ~= '' then package.path = workerPackagePath end if type(workerPackageCPath) == 'string' and workerPackageCPath ~= '' then package.cpath = workerPackageCPath end local function readAll(path) local file = io.open(path, 'rb') if not file then return nil end local text = file:read('*a') file:close() return text end local function writeAll(path, text) local file = io.open(path, 'wb') if not file then return false end file:write(text or '') file:close() return true end local text = nil if tostring(mode or '') == 'remote' then local okRequest, response = pcall(function() local requests = require 'requests' return requests.request('GET', tostring(url or ''), {timeout = 30, headers = {['User-Agent'] = 'Nakhodka'}}) end) if not okRequest or type(response) ~= 'table' then return false, 0, tostring(response or 'request') end local rawStatus = response.status_code or response.status or response.code local status = tonumber(rawStatus) or tonumber(tostring(rawStatus or ''):match('(%d%d%d)')) if status ~= nil and (status < 200 or status >= 300) then return false, 0, 'HTTP ' .. tostring(status) end text = response.text or response.content or response.body or response.data if type(text) ~= 'string' or #text == 0 then return false, 0, 'empty response' end if not writeAll(jsonPath, text) then return false, 0, 'cache write' end else text = readAll(jsonPath) end if type(text) ~= 'string' or text == '' then return false, 0, 'no price data' end local okJson, json = pcall(require, 'dkjson') if not okJson or type(json) ~= 'table' or type(json.decode) ~= 'function' then return false, 0, 'dkjson unavailable' end local okDecode, data = pcall(json.decode, text) if not okDecode or type(data) ~= 'table' then return false, 0, 'invalid json' end local rows = {} for name, entry in pairs(data) do local price = nil if type(entry) == 'table' and type(entry.list) == 'table' then for _, row in ipairs(entry.list) do if type(row) == 'table' then price = tonumber(row[6]) or tonumber(row[5]) or tonumber(row[3]) if price ~= nil and price > 0 then break end end end end if price == nil and type(entry) == 'table' then price = tonumber(entry.avg) or tonumber(entry.average) or tonumber(entry.price) end if price ~= nil and price > 0 then local safeName = tostring(name):gsub('[\r\n\t]', ' ') rows[#rows + 1] = safeName .. '\t' .. tostring(math.floor(price + 0.5)) end end table.sort(rows) if not writeAll(cachePath, table.concat(rows, '\n')) then return false, 0, 'compact cache write' end return true, #rows, '' end ]] function khBeginAveragePriceCacheApply(path, expectedCount, onDone, mode) if khAveragePriceCacheApplying ~= nil then return false end local file = io.open(tostring(path or ''), 'r') if not file then if type(onDone) == 'function' then pcall(onDone, false, 'cache missing') end return false end khAveragePriceCacheApplying = file khAveragePriceCacheFile = tostring(path or '') khAveragePriceCacheCount = tonumber(expectedCount) or 0 khAveragePriceApplyDone = onDone khAveragePriceApplyMode = mode khAveragePricesByName = khAveragePricesByName or {} return true end function khFinishAveragePriceCacheApply(ok, reason) if khAveragePriceCacheApplying ~= nil then pcall(khAveragePriceCacheApplying.close, khAveragePriceCacheApplying) end khAveragePriceCacheApplying = nil khAveragePriceCacheFile = nil khAveragePricesLoaded = true khAveragePriceItemCache = {} local callback = khAveragePriceApplyDone khAveragePriceApplyDone = nil khAveragePriceApplyMode = nil if type(callback) == 'function' then pcall(callback, ok and true or false, reason) end end function khProcessAveragePriceCacheApply(limit) local file = khAveragePriceCacheApplying if file == nil then return end limit = tonumber(limit) or 160 local processed = 0 while processed < limit do local line = file:read('*l') if line == nil then khFinishAveragePriceCacheApply(true) return end local name, price = line:match('^(.-)\t([%d%.]+)$') price = tonumber(price) if name ~= nil and name ~= '' and price ~= nil and price > 0 then khAveragePricesByName[khNormalizePriceName(name)] = math.floor(price + 0.5) end processed = processed + 1 end end function khStartAveragePriceWorker(mode, url, onDone) if khAveragePriceWorker ~= nil or khAveragePriceCacheApplying ~= nil then if type(onDone) == 'function' then pcall(onDone, false, 'already loading') end return false end if not khEffilReady or khEffil == nil or type(khEffil.thread) ~= 'function' then if type(onDone) == 'function' then pcall(onDone, false, 'effil unavailable') end return false end local okFactory, factory = pcall(function() return loadstring(khAveragePriceWorkerSource)() end) if not okFactory or type(factory) ~= 'function' then if type(onDone) == 'function' then pcall(onDone, false, 'worker create') end return false end local okThread, thread = pcall(function() return khEffil.thread(factory)(tostring(mode or 'cache'), tostring(url or ''), khAveragePricePath(), khAveragePriceCachePath(), tostring((package and package.path) or ''), tostring((package and package.cpath) or '')) end) if not okThread or thread == nil then if type(onDone) == 'function' then pcall(onDone, false, 'worker start') end return false end khAveragePriceWorker = thread lua_thread.create(function() local startedAt = os.clock() while khAveragePriceWorker == thread do local status, statusError = thread:status() if status == 'completed' then local okGet, workerOk, count, reason = pcall(thread.get, thread) khAveragePriceWorker = nil if not okGet or not workerOk then if type(onDone) == 'function' then pcall(onDone, false, reason or count or statusError or 'worker') end elseif not khBeginAveragePriceCacheApply(khAveragePriceCachePath(), count, onDone, mode) then if type(onDone) == 'function' then pcall(onDone, false, 'cache apply') end end return elseif status == 'failed' or status == 'canceled' or statusError ~= nil then khAveragePriceWorker = nil if type(onDone) == 'function' then pcall(onDone, false, statusError or status) end return elseif os.clock() - startedAt > 60 then pcall(function() thread:cancel(0) end) khAveragePriceWorker = nil if type(onDone) == 'function' then pcall(onDone, false, 'timeout') end return end wait(0) end end) return true end khDownloadFileWorkerSource = [[ return function(url, path, workerPackagePath, workerPackageCPath) if type(workerPackagePath) == 'string' and workerPackagePath ~= '' then package.path = workerPackagePath end if type(workerPackageCPath) == 'string' and workerPackageCPath ~= '' then package.cpath = workerPackageCPath end local okRequest, response = pcall(function() local requests = require 'requests' return requests.request('GET', tostring(url or ''), {timeout = 30, headers = {['User-Agent'] = 'Nakhodka'}}) end) if not okRequest or type(response) ~= 'table' then return false, 0, tostring(response or 'request') end local rawStatus = response.status_code or response.status or response.code local status = tonumber(rawStatus) or tonumber(tostring(rawStatus or ''):match('(%d%d%d)')) if status ~= nil and (status < 200 or status >= 300) then return false, 0, 'HTTP ' .. tostring(status) end local body = response.text or response.content or response.body or response.data if type(body) ~= 'string' or #body == 0 then return false, 0, 'empty response' end local file = io.open(tostring(path or ''), 'wb') if not file then return false, 0, 'file open' end file:write(body) file:close() return true, #body, '' end ]] function khAsyncDownloadFile(url, path, resolve, reject) resolve = resolve or function() end reject = reject or function() end if not khEffilReady or khEffil == nil or type(khEffil.thread) ~= 'function' then reject('effil недоступен') return false end local okFactory, factory = pcall(function() return loadstring(khDownloadFileWorkerSource)() end) if not okFactory or type(factory) ~= 'function' then reject('worker create'); return false end local okThread, thread = pcall(function() return khEffil.thread(factory)(tostring(url or ''), tostring(path or ''), tostring((package and package.path) or ''), tostring((package and package.cpath) or '')) end) if not okThread or thread == nil then reject('worker start'); return false end lua_thread.create(function() local startedAt = os.clock() while true do local status, statusError = thread:status() if status == 'completed' then local okGet, workerOk, size, reason = pcall(thread.get, thread) if okGet and workerOk then resolve(size) else reject(reason or size or statusError or 'download') end return elseif status == 'failed' or status == 'canceled' or statusError ~= nil then reject(statusError or status) return elseif os.clock() - startedAt > 60 then pcall(function() thread:cancel(0) end) reject('timeout') return end wait(0) end end) return true end function khApplyAveragePriceText(text) -- Kept for compatibility with old callers. The actual JSON work is done -- in khAveragePriceWorker and applied incrementally from the compact cache. if type(text) ~= 'string' or text == '' then return false end khWriteAveragePriceFile(text) return khStartAveragePriceWorker('cache', nil, function() end) 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 tryNextUrl() local url = urls[index] index = index + 1 if url == nil then khAvgPriceLoading = false if khStartAveragePriceWorker('cache', nil, function(ok, reason) if ok then nakhodkaNotify('Сохраненные средние цены применены без фриза.', -1, 'info', 3) else nakhodkaNotify('Ошибка загрузки средних цен: ' .. tostring(lastError or reason), -1, 'error', 4) end end) then return end nakhodkaNotify('Ошибка загрузки средних цен: ' .. tostring(lastError), -1, 'error', 4) return end if khStartAveragePriceWorker('remote', url, function(ok, reason) khAvgPriceLoading = false if ok then local items = khGetUniqueDropItems() local loaded = 0 for _, name in ipairs(items) do local price = khFindAveragePriceInMap(name, khAveragePricesByName) 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() nakhodkaNotify(string.format('Средние цены загружены: %d из %d.', loaded, #items), -1, 'success', 4) else lastError = tostring(reason or 'download') tryNextUrl() end end) then return end lastError = 'worker start' tryNextUrl() 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 function khParseKladCountAndName(text) local raw = khDropDecodeUtf8(clearNotifyText(text)) raw = raw:gsub('%s*Откройте.+$', ''):gsub('%s+', ' '):gsub('^%s+', ''):gsub('%s+$', '') local sht = '[' .. string.char(248, 216) .. '][' .. string.char(242, 210) .. ']' local xmark = '[xX' .. string.char(245) .. string.char(213) .. ']' local count, name = raw:match('^(%d+)%s*[%._%-%/%s]*' .. sht .. '[%.%s]*%s*(.+)$') if count and name then return tonumber(count) or 1, khCleanDropItemName(name) end count, name = raw:match('^' .. xmark .. '?(%d+)%s+(.+)$') if count and name then return tonumber(count) or 1, khCleanDropItemName(name) end name, count = raw:match('^(.+)%s+(%d+)%s*[%._%-%/%s]*' .. sht .. '[%.%s]*$') if count and name then return tonumber(count) or 1, khCleanDropItemName(name) end name, count = raw:match('^(.+)%s*' .. xmark .. '(%d+)$') if count and name then return tonumber(count) or 1, khCleanDropItemName(name) end name, count = raw:match('^(.+)%s*%(%s*(%d+)%s*' .. sht .. '%.?%s*%)$') if count and name then return tonumber(count) or 1, khCleanDropItemName(name) end name, count = raw:match('^(.+)%s*%[%s*(%d+)%s*' .. sht .. '%.?%s*%]$') if count and name then return tonumber(count) or 1, khCleanDropItemName(name) end return 1, khCleanDropItemName(raw) end function khParseItemNameAndCount(text) local count, name = khParseKladCountAndName(text) if name == '' then return nil, nil end return name, math.max(1, math.floor((tonumber(count) or 1) + 0.5)) end function khParseDropMessage(message) message = clearNotifyText(message) local itemText = message:match('Вы%s+успешно%s+достали%s+из%s+клада%s+(.+)') if not itemText then return nil, nil end return khParseItemNameAndCount(itemText) end function khEnsurePendingDropSession() if khPendingDropSession == nil then local capture = khGetMainDigCaptureForReport and khGetMainDigCaptureForReport() or nil 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(), lastDropTime = now, digLocation = capture and khApiCopyPosition(capture.reportPosition) or nil, actualDigLocation = capture and khApiCopyPosition(capture.actualPosition) or nil, digId = capture and capture.digId or nil, nearestPointId = capture and capture.nearestPointId or nil, captureSource = capture and capture.source or nil } end return khPendingDropSession end function khAddPendingDropItem(itemName, count, keepDropClock) itemName = khCleanDropItemName(itemName) count = math.max(1, math.floor((tonumber(count) or 1) + 0.5)) if itemName == '' then return end local session = khEnsurePendingDropSession() if not keepDropClock then session.lastDropClock = os.clock() session.lastDropTime = os.time() khLastDropAt = session.lastDropClock end for _, item in ipairs(session.items) do if tostring(item.name) == tostring(itemName) then item.count = (tonumber(item.count) or 1) + count return end end table.insert(session.items, {name = itemName, count = count}) end -- The client report is untrusted telemetry. It never proves a dig or increments -- the public counter by itself; the server stores it as pending for moderation. NK_API_BASE_URL = 'https://nakhodka.fun/api' khApiQueue = {} khApiInstallId = '' khApiSessionToken = '' khApiSessionExpiresAt = 0 khApiMovementSessionId = '' khApiRequestBusy = false khApiNextAttemptAt = 0 khApiConsecutiveFailures = 0 khDropDigLocation = nil khMainDigCapture = nil khMainDigCaptureMaxAge = 8.0 function khApiCopyPosition(point) if type(point) ~= 'table' then return nil end local x, y, z = tonumber(point.x), tonumber(point.y), tonumber(point.z) if x == nil or y == nil or z == nil then return nil end return {x = x, y = y, z = z} end function khApiIsUuid(value) value = tostring(value or ''):lower() return value:match('^[%x][%x][%x][%x][%x][%x][%x][%x]%-%x%x%x%x%-[1-5]%x%x%x%-[89ab]%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$') ~= nil end function khResolveMainDigPosition(actualPosition) local actual = khApiCopyPosition(actualPosition) if actual == nil then return nil, nil, nil end local bestPoint, bestId, bestDistance = nil, nil, 25.0 for index, point in ipairs(khMainTreasurePoints or {}) do local x, y, z = tonumber(point.x), tonumber(point.y), tonumber(point.z) if x ~= nil and y ~= nil and z ~= nil then local dx, dy, dz = actual.x - x, actual.y - y, actual.z - z local distance = dx * dx + dy * dy + dz * dz if distance <= bestDistance then bestDistance = distance bestPoint = {x = x, y = y, z = z} bestId = tonumber(point.id) or index end end end return bestPoint or actual, actual, bestId end function khClearMainDigCapture() khMainDigCapture = nil khDropDigLocation = nil end function khCaptureDropDigLocation() -- Main point reports must never use khLastRaceCheckpoint: it may belong to -- a waypoint, spawn search or another script. Capture the player directly. if type(getCharCoordinates) ~= 'function' then return nil end local ok, x, y, z = pcall(getCharCoordinates, PLAYER_PED) if not ok or x == nil or y == nil or z == nil then return nil end local reportPosition, actualPosition, nearestPointId = khResolveMainDigPosition({ x = tonumber(x), y = tonumber(y), z = tonumber(z) }) if reportPosition == nil then return nil end khMainDigCapture = { reportPosition = khApiCopyPosition(reportPosition), actualPosition = khApiCopyPosition(actualPosition), nearestPointId = nearestPointId, capturedAt = os.clock(), source = 'excavations_click', armed = false, rewardConfirmed = false, digId = nil } khDropDigLocation = khApiCopyPosition(reportPosition) return khApiCopyPosition(reportPosition) end function khArmMainDigCapture() local now = os.clock() if khMainDigCapture == nil or now - (tonumber(khMainDigCapture.capturedAt) or 0) > khMainDigCaptureMaxAge then khCaptureDropDigLocation() end local capture = khMainDigCapture if type(capture) ~= 'table' or capture.reportPosition == nil then return false end local digId = khApiNewUuid() if not khApiIsUuid(digId) then return false end capture.digId = digId capture.armed = true capture.rewardConfirmed = false capture.armedAt = now capture.rewardDeadline = now + 5.0 return true end function khConfirmMainDigCapture() local capture = khMainDigCapture local now = os.clock() if type(capture) ~= 'table' or capture.armed ~= true then return false end if now > (tonumber(capture.rewardDeadline) or 0) then khClearMainDigCapture() return false end capture.rewardConfirmed = true capture.confirmedAt = now return true end function khCreateConfirmedFallbackCapture() local position = khCaptureDropDigLocation() local capture = khMainDigCapture if position == nil or type(capture) ~= 'table' then return nil end local digId = khApiNewUuid() if not khApiIsUuid(digId) then khClearMainDigCapture() return nil end capture.digId = digId capture.armed = true capture.rewardConfirmed = true capture.armedAt = os.clock() capture.confirmedAt = capture.armedAt capture.rewardDeadline = capture.armedAt + 5.0 capture.source = 'reward_fallback' return capture end function khGetMainDigCaptureForReport() local capture = khMainDigCapture if type(capture) ~= 'table' or capture.rewardConfirmed ~= true or not khApiIsUuid(capture.digId) then return nil end if os.clock() - (tonumber(capture.confirmedAt) or 0) > 120.0 then khClearMainDigCapture() return nil end return capture end function khExpireMainDigCapture(nowClock) local capture = khMainDigCapture if type(capture) ~= 'table' then return end local now = tonumber(nowClock) or os.clock() if capture.rewardConfirmed == true then if now - (tonumber(capture.confirmedAt) or now) > 120.0 then khClearMainDigCapture() end elseif capture.armed == true and now > (tonumber(capture.rewardDeadline) or 0) then khClearMainDigCapture() elseif capture.armed ~= true and now - (tonumber(capture.capturedAt) or now) > khMainDigCaptureMaxAge then khClearMainDigCapture() end end function khApiNewUuid() local ok, crypto = pcall(require, 'crypto_lua') if not ok or type(crypto) ~= 'table' or type(crypto.salsa20_generate_key) ~= 'function' then return nil end local created, key = pcall(crypto.salsa20_generate_key) if not created or type(key) ~= 'string' or #key < 16 then return nil end local digest = nkBootSha256Data(key) if type(digest) ~= 'string' or #digest < 32 then return nil end return string.format('%s-%s-4%s-8%s-%s', digest:sub(1, 8), digest:sub(9, 12), digest:sub(13, 15), digest:sub(17, 19), digest:sub(20, 31)) end function khApiResetMovementSession() local value = khApiNewUuid() if khApiIsUuid(value) then khApiMovementSessionId = value end return khApiMovementSessionId end khApiResetMovementSession() function khApiSha256(message) if khBit == nil then return nil end local band, bor, bxor, rshift, lshift, ror = khBit.band, khBit.bor, khBit.bxor, khBit.rshift, khBit.lshift, khBit.ror if type(band) ~= 'function' or type(bor) ~= 'function' or type(bxor) ~= 'function' or type(rshift) ~= 'function' or type(lshift) ~= 'function' or type(ror) ~= 'function' then return nil end local function add32(...) local total = 0 for index = 1, select('#', ...) do total = (total + (tonumber((select(index, ...))) or 0)) % 4294967296 end return total end local constants = { 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 } message = tostring(message or '') local bitLength = #message * 8 message = message .. string.char(128) while (#message % 64) ~= 56 do message = message .. string.char(0) end local high = math.floor(bitLength / 4294967296) local low = bitLength % 4294967296 local function word(value) return string.char( math.floor(value / 16777216) % 256, math.floor(value / 65536) % 256, math.floor(value / 256) % 256, value % 256 ) end message = message .. word(high) .. word(low) local h0,h1,h2,h3,h4,h5,h6,h7 = 0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19 for offset = 1, #message, 64 do local w = {} for index = 0, 15 do local base = offset + index * 4 w[index] = bor(lshift(message:byte(base), 24), lshift(message:byte(base + 1), 16), lshift(message:byte(base + 2), 8), message:byte(base + 3)) end for index = 16, 63 do local a, b = w[index - 15], w[index - 2] local s0 = bxor(ror(a, 7), ror(a, 18), rshift(a, 3)) local s1 = bxor(ror(b, 17), ror(b, 19), rshift(b, 10)) w[index] = add32(w[index - 16], s0, w[index - 7], s1) end local a,b,c,d,e,f,g,h = h0,h1,h2,h3,h4,h5,h6,h7 for index = 0, 63 do local s1 = bxor(ror(e, 6), ror(e, 11), ror(e, 25)) local choose = bxor(band(e, f), band(bxor(e, -1), g)) local t1 = add32(h, s1, choose, constants[index + 1], w[index]) local s0 = bxor(ror(a, 2), ror(a, 13), ror(a, 22)) local majority = bxor(band(a, b), band(a, c), band(b, c)) local t2 = add32(s0, majority) h,g,f,e,d,c,b,a = g,f,e,add32(d, t1),c,b,a,add32(t1, t2) end h0,h1,h2,h3,h4,h5,h6,h7 = add32(h0,a),add32(h1,b),add32(h2,c),add32(h3,d),add32(h4,e),add32(h5,f),add32(h6,g),add32(h7,h) end return khBit.tohex(h0, 8) .. khBit.tohex(h1, 8) .. khBit.tohex(h2, 8) .. khBit.tohex(h3, 8) .. khBit.tohex(h4, 8) .. khBit.tohex(h5, 8) .. khBit.tohex(h6, 8) .. khBit.tohex(h7, 8) end function khApiEnsureInstallId() if mainCfg == nil then return false end if mainCfg.Api == nil then mainCfg.Api = {} end local id = tostring(mainCfg.Api.installId or ''):lower() if not id:match('^[%x][%x][%x][%x][%x][%x][%x][%x]%-%x%x%x%x%-[1-5]%x%x%x%-[89ab]%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$') then local generated = khApiNewUuid() if type(generated) ~= 'string' then return false end id = generated:lower() mainCfg.Api.installId = id pcall(inicfg.save, mainCfg, config_file) end khApiInstallId = id return id ~= '' end function khApiResponseData(response) local status, text = nil, nil if type(response) == 'table' then status = tonumber(response.status_code or response.status) text = response.text or response.content or response.body or response.data elseif type(response) == 'string' then text = response else return nil end if status ~= nil and status > 0 and (status < 200 or status >= 300) then return nil end if type(text) ~= 'string' or text == '' then return nil end if 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 -- API возвращает короткий плоский JSON. Fallback не зависит от dkjson, -- чтобы успешная сессия не терялась из-за сериализации worker-потока. if text:match('"ok"%s*:%s*true') then return { ok = true, token = text:match('"token"%s*:%s*"([%x]+)"'), expires_at = tonumber(text:match('"expires_at"%s*:%s*(%d+)')), count = tonumber(text:match('"count"%s*:%s*(%d+)')) } end return nil end function khApiFail() khApiRequestBusy = false khApiConsecutiveFailures = (tonumber(khApiConsecutiveFailures) or 0) + 1 khApiNextAttemptAt = os.clock() + (khApiConsecutiveFailures >= 3 and 600 or 60) end function khApiStartSession() if khApiRequestBusy or not khApiEnsureInstallId() or type(khAsyncHttpRequest) ~= 'function' then return false end khApiRequestBusy = true local nickname = '' if type(khGetMyNickname) == 'function' then local ok, value = pcall(khGetMyNickname) if ok then nickname = tostring(value or '') end end if nickname == '' then nickname = 'Nakhodka' end local payload = { install_id = khApiInstallId, script_version = tostring(NK_SCRIPT_VERSION_TEXT or NK_SCRIPT_VERSION or ''), nickname = nickname } local ok = pcall(khAsyncHttpRequest, 'POST', NK_API_BASE_URL .. '/session.php', { timeout = 8, kh_thread_timeout = 12, headers = {['Content-Type'] = 'application/json', ['User-Agent'] = 'Nakhodka'}, data = khJson and khJson.encode(payload) or '' }, function(response) local data = khApiResponseData(response) if type(data) == 'table' and data.ok == true and type(data.token) == 'string' and #data.token == 64 then khApiSessionToken = data.token khApiSessionExpiresAt = tonumber(data.expires_at) or (os.time() + 21600) khApiRequestBusy = false khApiConsecutiveFailures = 0 khApiNextAttemptAt = os.clock() else khApiFail() end end, function() khApiFail() end) if not ok then khApiFail() end return ok end function khApiNearestPointId(position) if type(position) ~= 'table' then return nil end local bestId, bestDistance = nil, 49.0 for index, point in ipairs(khMainTreasurePoints or {}) do local x, y, z = tonumber(point.x), tonumber(point.y), tonumber(point.z) if x ~= nil and y ~= nil and z ~= nil then local dx, dy, dz = position.x - x, position.y - y, position.z - z local distance = dx * dx + dy * dy + dz * dz if distance <= bestDistance then bestDistance = distance bestId = tonumber(point.id) or index end end end return bestId end function khApiResolveTreasureReportPosition(metadata) metadata = type(metadata) == 'table' and metadata or {} local position = khApiCopyPosition(metadata.digLocation) or khApiCopyPosition(metadata.actualDigLocation) if position == nil and type(khGetMainDigCaptureForReport) == 'function' then local capture = khGetMainDigCaptureForReport() if type(capture) == 'table' then position = khApiCopyPosition(capture.reportPosition) or khApiCopyPosition(capture.actualPosition) end end if position == nil then position = khApiCopyPosition(khDropDigLocation) end if position == nil and type(getCharCoordinates) == 'function' then local ok, x, y, z = pcall(getCharCoordinates, PLAYER_PED) if ok then position = khApiCopyPosition({x = x, y = y, z = z}) end end return position end function khApiQueueTreasureReport(metadata) metadata = type(metadata) == 'table' and metadata or {} local position = khApiResolveTreasureReportPosition(metadata) if position == nil then return false end local digId = tostring(metadata.digId or metadata.dig_id or ''):lower() if not khApiIsUuid(digId) then digId = khApiNewUuid() end if not khApiIsUuid(digId) then return false end local actual = khApiCopyPosition(metadata.actualDigLocation) or position local report = { dig_id = digId, x = position.x, y = position.y, z = position.z, actual_x = actual.x, actual_y = actual.y, actual_z = actual.z, nearest_point_id = tonumber(metadata.nearestPointId) or khApiNearestPointId(position), capture_source = tostring(metadata.captureSource or 'reward_fallback'), movement_session_id = tostring(khApiMovementSessionId or '') } if #khApiQueue >= 20 then table.remove(khApiQueue, 1) end table.insert(khApiQueue, report) return true end function khApiSendQueuedDig() if khApiRequestBusy or #khApiQueue == 0 or khApiSessionToken == '' then return false end local report = khApiQueue[1] local timestamp = os.time() local payload = { dig_id = report.dig_id, install_id = khApiInstallId, session_token = khApiSessionToken, movement_session_id = report.movement_session_id, x = tonumber(string.format('%.3f', report.x)), y = tonumber(string.format('%.3f', report.y)), z = tonumber(string.format('%.3f', report.z)), actual_x = tonumber(string.format('%.3f', report.actual_x or report.x)), actual_y = tonumber(string.format('%.3f', report.actual_y or report.y)), actual_z = tonumber(string.format('%.3f', report.actual_z or report.z)), ts = timestamp, nearest_point_id = report.nearest_point_id, capture_source = report.capture_source, script_version = tostring(NK_SCRIPT_VERSION_TEXT or NK_SCRIPT_VERSION or '') } khApiRequestBusy = true local ok = pcall(khAsyncHttpRequest, 'POST', NK_API_BASE_URL .. '/dig.php', { timeout = 8, kh_thread_timeout = 12, headers = {['Content-Type'] = 'application/json', ['User-Agent'] = 'Nakhodka'}, data = khJson and khJson.encode(payload) or '' }, function(response) local data = khApiResponseData(response) khApiRequestBusy = false if type(data) == 'table' and data.ok == true then table.remove(khApiQueue, 1) khApiConsecutiveFailures = 0 khApiNextAttemptAt = os.clock() + 60 else khApiFail() end end, function() khApiFail() end) if not ok then khApiFail() end return ok end function khApiTick(nowClock) if khApiRequestBusy or nowClock < (tonumber(khApiNextAttemptAt) or 0) then return end if khApiSessionToken == '' or os.time() >= (tonumber(khApiSessionExpiresAt) or 0) - 30 then khApiStartSession() elseif #khApiQueue > 0 then khApiSendQueuedDig() end end function khReportDig(metadata) return khApiQueueTreasureReport(metadata) end function khQueueTreasureReportOnce(metadata) metadata = type(metadata) == 'table' and metadata or {} if metadata.siteReportQueued == true then return true end local ok, queued = pcall(khReportDig, metadata) if ok and queued then metadata.siteReportQueued = true return true end return false end -- Reliable dig delivery. A CEF reward title is the only final confirmation; -- chat messages and click events only supply context for the local drop log. khDigState = 'IDLE' khDigAttemptId = 0 khApiQueueLoaded = false khApiQueueMaxSize = 500 function khApiDebug(message) if khDropDebug == true then print('[Nakhodka API] ' .. tostring(message or '')) end end function khApiGetNickname() if type(khGetMyNickname) == 'function' then local ok, value = pcall(khGetMyNickname) if ok and tostring(value or '') ~= '' then return tostring(value) end end return 'Nakhodka' end function khApiQueuePath() local base = tostring(config_file or ''):match('^(.*)[/\\][^/\\]+$') if base == nil or base == '' then local ok, working = pcall(getWorkingDirectory) base = ok and tostring(working or '') or '.' end return base .. '\\nakhodka_api_queue.json' end function khApiSaveQueue() if khJson == nil or type(khJson.encode) ~= 'function' then return false end local path = khApiQueuePath() local directory = path:match('^(.*)[/\\][^/\\]+$') if directory ~= nil and directory ~= '' then pcall(createDirectory, directory) end local ok, text = pcall(khJson.encode, {version = 1, items = khApiQueue}) if not ok or type(text) ~= 'string' then return false end local temp = path .. '.tmp' local file = io.open(temp, 'wb') if file == nil then return false end file:write(text) file:close() os.remove(path) if not os.rename(temp, path) then os.remove(temp) return false end return true end function khApiLoadQueue() if khApiQueueLoaded then return end khApiQueueLoaded = true khApiQueue = {} if khJson == nil or type(khJson.decode) ~= 'function' then return end local file = io.open(khApiQueuePath(), 'rb') if file == nil then return end local text = file:read('*a') file:close() local ok, decoded = pcall(khJson.decode, text or '') local items = ok and type(decoded) == 'table' and decoded.items or nil if type(items) ~= 'table' then return end for _, entry in ipairs(items) do if #khApiQueue >= khApiQueueMaxSize then break end if type(entry) == 'table' and khApiIsUuid(entry.dig_id) and (entry.kind == 'dig' or entry.kind == 'loot') then entry.session_token = nil entry.attempts = math.max(0, math.floor(tonumber(entry.attempts) or 0)) entry.next_attempt_at = 0 table.insert(khApiQueue, entry) end end if type(khApiRecoverCompatibleDeadLetters) == 'function' then pcall(khApiRecoverCompatibleDeadLetters) end khApiDebug('queue restored: ' .. tostring(#khApiQueue)) end function khApiQueueFind(digId, kind) local wantedId = tostring(digId or ''):lower() for _, entry in ipairs(khApiQueue) do if tostring(entry.dig_id or ''):lower() == wantedId and entry.kind == kind then return entry end end return nil end function khApiDeadLetterPath() return khApiQueuePath():gsub('%.json$', '') .. '_dead.json' end khApiDeadRecoveryDone = false function khApiRecoverCompatibleDeadLetters() if khApiDeadRecoveryDone then return end khApiDeadRecoveryDone = true if khJson == nil or type(khJson.decode) ~= 'function' then return end local path = khApiDeadLetterPath() local file = io.open(path, 'rb') if file == nil then return end local text = file:read('*a'); file:close() local ok, decoded = pcall(khJson.decode, text or '') if not ok or type(decoded) ~= 'table' or type(decoded.items) ~= 'table' then return end local kept, recovered = {}, 0 for _, row in ipairs(decoded.items) do local entry = type(row) == 'table' and row.entry or nil local recent = os.time() - (tonumber(row and row.saved_at) or 0) <= 604800 local entryKind = type(entry) == 'table' and tostring(entry.kind or '') or '' local rowStatus = tonumber(row and row.status) or 0 local recoverableStatus = rowStatus == 400 or (entryKind == 'loot' and rowStatus == 409) if recent and type(entry) == 'table' and (entryKind == 'dig' or entryKind == 'loot') and khApiIsUuid(entry.dig_id) and recoverableStatus and khApiQueueFind(entry.dig_id, entryKind) == nil and #khApiQueue < khApiQueueMaxSize then entry.attempts = 0; entry.next_attempt_at = 0 table.insert(khApiQueue, entry); recovered = recovered + 1 else table.insert(kept, row) end end if recovered > 0 then local encoded = khJson.encode({version = 1, items = kept}) local out = io.open(path .. '.tmp', 'wb') if out ~= nil then out:write(encoded); out:close(); os.remove(path); os.rename(path .. '.tmp', path) end khApiSaveQueue() khApiDebug('recovered compatible API events: ' .. tostring(recovered)) end end function khApiSaveDeadLetter(entry, status, data, reason) if type(entry) ~= 'table' or khJson == nil or type(khJson.encode) ~= 'function' then return end local path = khApiDeadLetterPath() local rows = {} local file = io.open(path, 'rb') if file ~= nil then local text = file:read('*a') file:close() local ok, decoded = pcall(khJson.decode, text or '') if ok and type(decoded) == 'table' and type(decoded.items) == 'table' then rows = decoded.items end end table.insert(rows, { entry = entry, status = tonumber(status) or 0, reason = tostring(reason or (type(data) == 'table' and (data.reason or data.error)) or 'rejected'), saved_at = os.time() }) while #rows > 100 do table.remove(rows, 1) end local ok, encoded = pcall(khJson.encode, {version = 1, items = rows}) if not ok then return end local temp = path .. '.tmp' local out = io.open(temp, 'wb') if out == nil then return end out:write(encoded) out:close() os.remove(path) os.rename(temp, path) end function khApiQueueRemoveAt(index) index = tonumber(index) if index == nil or index < 1 or index > #khApiQueue then return nil end local removed = table.remove(khApiQueue, index) khApiSaveQueue() return removed end function khApiQueueMoveToDead(index, status, data, reason) local entry = khApiQueue[index] if type(entry) ~= 'table' then return false end khApiSaveDeadLetter(entry, status, data, reason) khApiQueueRemoveAt(index) khApiDebug('event moved to dead-letter: ' .. tostring(reason or status)) return true end function khApiQueueFindReady(now) now = tonumber(now) or os.clock() local function hasPendingDig(digId) for _, candidate in ipairs(khApiQueue) do if candidate.kind == 'dig' and candidate.dig_id == digId then return true end end return false end -- Dig reports are more important than loot and must never be blocked by an -- old rate-limited event at the head of the persistent queue. for index, entry in ipairs(khApiQueue) do if entry.kind == 'dig' and now >= (tonumber(entry.next_attempt_at) or 0) then return index, entry end end for index, entry in ipairs(khApiQueue) do if entry.kind == 'loot' and not hasPendingDig(entry.dig_id) and now >= (tonumber(entry.next_attempt_at) or 0) then return index, entry end end return nil, nil end function khApiQueuePush(entry) khApiLoadQueue() if type(entry) ~= 'table' or not khApiIsUuid(entry.dig_id) then return false end if khApiQueueFind(entry.dig_id, entry.kind) ~= nil then return true end if #khApiQueue >= khApiQueueMaxSize then -- Discard the oldest loot snapshot first; confirmed digs are never -- silently deleted. The local drop journal still keeps the loot. for index, old in ipairs(khApiQueue) do if old.kind == 'loot' then table.remove(khApiQueue, index); break end end end if #khApiQueue >= khApiQueueMaxSize then khApiDebug('queue is full; confirmed dig kept for the next reload') return false end table.insert(khApiQueue, entry) khApiSaveQueue() return true end function khCaptureDropDigLocation() if type(getCharCoordinates) ~= 'function' then return nil end local ok, x, y, z = pcall(getCharCoordinates, PLAYER_PED) if not ok or x == nil or y == nil or z == nil then return nil end local reportPosition, actualPosition, nearestPointId = khResolveMainDigPosition({x = x, y = y, z = z}) if reportPosition == nil then return nil end khMainDigCapture = { reportPosition = khApiCopyPosition(reportPosition), actualPosition = khApiCopyPosition(actualPosition), nearestPointId = nearestPointId, capturedAt = os.clock(), source = 'successful_space_click', state = 'IDLE' } khDropDigLocation = khApiCopyPosition(reportPosition) return khApiCopyPosition(reportPosition) end function khArmMainDigCapture() khCaptureDropDigLocation() local capture = khMainDigCapture if type(capture) ~= 'table' or capture.reportPosition == nil then return false end local digId, eventId = khApiNewUuid(), khApiNewUuid() if not khApiIsUuid(digId) or not khApiIsUuid(eventId) then khClearMainDigCapture() return false end khDigAttemptId = khDigAttemptId + 1 capture.digId = digId capture.eventId = eventId capture.attemptId = khDigAttemptId capture.movementSessionId = tostring(khApiMovementSessionId or '') capture.armed = true capture.rewardConfirmed = false capture.armedAt = os.clock() capture.rewardDeadline = capture.armedAt + 5.0 capture.state = 'WAITING_REWARD_CONFIRMATION' khDigState = capture.state khApiDebug('attempt ' .. tostring(capture.attemptId) .. ' armed') return true end function khApiQueueTreasureReport(metadata) metadata = type(metadata) == 'table' and metadata or {} local digId = tostring(metadata.digId or metadata.dig_id or ''):lower() local eventId = tostring(metadata.eventId or metadata.event_id or ''):lower() local position = khApiCopyPosition(metadata.reportPosition or metadata.digLocation) local actual = khApiCopyPosition(metadata.actualPosition or metadata.actualDigLocation) if not khApiIsUuid(digId) or not khApiIsUuid(eventId) or position == nil or actual == nil then return false end local existing = khApiQueueFind(digId, 'dig') if existing ~= nil then return true end return khApiQueuePush({ kind = 'dig', dig_id = digId, event_id = eventId, movement_session_id = tostring(metadata.movementSessionId or khApiMovementSessionId or ''), x = position.x, y = position.y, z = position.z, actual_x = actual.x, actual_y = actual.y, actual_z = actual.z, nearest_point_id = tonumber(metadata.nearestPointId), capture_source = tostring(metadata.source or 'cef_reward_title'), timestamp = tonumber(metadata.timestamp) or os.time(), script_version = tostring(NK_SCRIPT_VERSION_TEXT or NK_SCRIPT_VERSION or ''), nickname = khApiGetNickname(), attempts = 0, next_attempt_at = 0 }) end function khConfirmMainDigCapture() local capture = khMainDigCapture local now = os.clock() if type(capture) ~= 'table' or capture.state ~= 'WAITING_REWARD_CONFIRMATION' then return false end if now > (tonumber(capture.rewardDeadline) or 0) then khClearMainDigCapture() khDigState = 'IDLE' return false end capture.rewardConfirmed = true capture.confirmedAt = now capture.state = 'CONFIRMED' khDigState = capture.state if khApiQueueTreasureReport(capture) then capture.queued = true capture.state = 'QUEUED' khDigState = capture.state khApiDebug('CEF treasure confirmed: ' .. tostring(capture.digId)) return true end khApiDebug('CEF treasure confirmed but queue is not available') return false end function khGetMainDigCaptureForReport() local capture = khMainDigCapture if type(capture) ~= 'table' or capture.rewardConfirmed ~= true or not khApiIsUuid(capture.digId) then return nil end if os.clock() - (tonumber(capture.confirmedAt) or 0) > 180.0 then khClearMainDigCapture() khDigState = 'IDLE' return nil end return capture end function khExpireMainDigCapture(nowClock) local capture = khMainDigCapture if type(capture) ~= 'table' then return end local now = tonumber(nowClock) or os.clock() if capture.state == 'WAITING_REWARD_CONFIRMATION' and now > (tonumber(capture.rewardDeadline) or 0) then khApiDebug('attempt expired without CEF reward') khClearMainDigCapture() khDigState = 'IDLE' elseif capture.state == 'CONFIRMED' and not capture.queued then if khApiQueueTreasureReport(capture) then capture.queued = true capture.state = 'QUEUED' khDigState = capture.state end elseif capture.state == 'DELIVERED' and now - (tonumber(capture.deliveredAt) or now) > 60.0 then khClearMainDigCapture() khDigState = 'IDLE' end end function khApiParseResponse(response) local status = type(response) == 'table' and tonumber(response.status_code) or 0 local text = type(response) == 'table' and tostring(response.text or '') or '' local data = nil if text ~= '' and khJson ~= nil and type(khJson.decode) == 'function' then local ok, decoded = pcall(khJson.decode, text) if ok and type(decoded) == 'table' then data = decoded end end return status, data, text end function khApiScheduleRetry(entry, status, data, reason) khApiRequestBusy = false if type(entry) ~= 'table' then return end entry.attempts = math.max(0, math.floor(tonumber(entry.attempts) or 0)) + 1 local delay = math.min(300, math.max(2, 2 ^ math.min(entry.attempts - 1, 7))) if tonumber(status) == 429 and type(data) == 'table' then delay = math.max(1, tonumber(data.retry_after) or delay) end if tonumber(status) == 401 then khApiSessionToken = '' khApiSessionExpiresAt = 0 delay = 1 end entry.next_attempt_at = os.clock() + delay khApiNextAttemptAt = os.clock() + (tonumber(status) == 401 and 1 or 0.15) khApiSaveQueue() khApiDebug('retry ' .. tostring(entry.kind) .. ' in ' .. tostring(delay) .. 's: ' .. tostring(reason or status)) end function khApiStartSession() if khApiRequestBusy or not khApiEnsureInstallId() or type(lua_thread) ~= 'table' or type(lua_thread.create) ~= 'function' or type(khTeamHttpPostRaw) ~= 'function' then return false end local payload = { install_id = khApiInstallId, script_version = tostring(NK_SCRIPT_VERSION_TEXT or NK_SCRIPT_VERSION or ''), nickname = khApiGetNickname() } local okEncode, encoded = pcall(function() return khJson and khJson.encode(payload) or '' end) if not okEncode or type(encoded) ~= 'string' or encoded == '' then return false end khApiRequestBusy = true local okStart = pcall(lua_thread.create, function() local okRun, runError = pcall(function() local ok, status, body, reason = khTeamHttpPostRaw(NK_API_BASE_URL .. '/session.php', encoded, 8) local data = nil if body ~= '' and khJson ~= nil and type(khJson.decode) == 'function' then local decodedOk, decoded = pcall(khJson.decode, body) if decodedOk and type(decoded) == 'table' then data = decoded end end khApiRequestBusy = false if ok and status == 200 and type(data) == 'table' and data.ok == true and type(data.token) == 'string' and #data.token == 64 then khApiSessionToken = data.token khApiSessionExpiresAt = tonumber(data.expires_at) or (os.time() + 21600) khApiConsecutiveFailures = 0 khApiNextAttemptAt = os.clock() khApiDebug('scalar session accepted') else khApiSessionToken = '' khApiSessionExpiresAt = 0 khApiConsecutiveFailures = khApiConsecutiveFailures + 1 local delay = type(data) == 'table' and tonumber(data.retry_after) or nil khApiNextAttemptAt = os.clock() + math.max(2, delay or math.min(120, 2 ^ math.min(khApiConsecutiveFailures, 6))) khApiDebug('scalar session failed: ' .. tostring(status) .. ' ' .. tostring(reason or '')) end end) if not okRun then khApiRequestBusy = false khApiSessionToken = '' khApiSessionExpiresAt = 0 khApiConsecutiveFailures = khApiConsecutiveFailures + 1 khApiNextAttemptAt = os.clock() + math.min(120, math.max(2, 2 ^ math.min(khApiConsecutiveFailures, 6))) khApiDebug('scalar session worker error: ' .. tostring(runError)) end end) if not okStart then khApiRequestBusy = false khApiNextAttemptAt = os.clock() + 2 return false end return true end function khApiSendQueuedDig() khApiLoadQueue() if khApiRequestBusy or #khApiQueue == 0 or khApiSessionToken == '' or type(lua_thread) ~= 'table' or type(lua_thread.create) ~= 'function' or type(khTeamHttpPostRaw) ~= 'function' then return false end local entryIndex, entry = khApiQueueFindReady(os.clock()) if entry == nil then return false end local endpoint, payload = nil, nil if entry.kind == 'dig' then endpoint = '/dig.php' payload = { dig_id = entry.dig_id, event_id = entry.event_id, install_id = khApiInstallId, session_token = khApiSessionToken, movement_session_id = entry.movement_session_id, x = entry.x, y = entry.y, z = entry.z, actual_x = entry.actual_x, actual_y = entry.actual_y, actual_z = entry.actual_z, nearest_point_id = entry.nearest_point_id, capture_source = entry.capture_source, timestamp = entry.timestamp, script_version = entry.script_version, nickname = entry.nickname } elseif entry.kind == 'loot' then endpoint = '/loot.php' payload = { dig_id = entry.dig_id, event_id = entry.event_id, install_id = khApiInstallId, session_token = khApiSessionToken, nickname = entry.nickname, items = entry.items } else khApiQueueMoveToDead(entryIndex, 0, nil, 'unknown_kind') return false end local utf8Payload = type(khTeamPacketToUtf8) == 'function' and khTeamPacketToUtf8(payload) or payload local okEncode, encoded = pcall(function() return khJson and khJson.encode(utf8Payload) or '' end) if not okEncode or type(encoded) ~= 'string' or encoded == '' then return false end khApiRequestBusy = true local okStart = pcall(lua_thread.create, function() local okRun, runError = pcall(function() local ok, status, body, reason = khTeamHttpPostRaw(NK_API_BASE_URL .. endpoint, encoded, 8) local data = nil if body ~= '' and khJson ~= nil and type(khJson.decode) == 'function' then local decodedOk, decoded = pcall(khJson.decode, body) if decodedOk and type(decoded) == 'table' then data = decoded end end if ok and status == 200 and type(data) == 'table' and data.ok == true then khApiRequestBusy = false for index, queued in ipairs(khApiQueue) do if queued.kind == entry.kind and tostring(queued.dig_id):lower() == tostring(entry.dig_id):lower() then khApiQueueRemoveAt(index) break end end khApiConsecutiveFailures = 0 khApiNextAttemptAt = os.clock() + 0.15 if entry.kind == 'dig' and type(khMainDigCapture) == 'table' and tostring(khMainDigCapture.digId):lower() == tostring(entry.dig_id):lower() then khMainDigCapture.state = 'DELIVERED' khMainDigCapture.deliveredAt = os.clock() khDigState = 'DELIVERED' end khApiDebug('scalar delivery accepted: ' .. tostring(entry.kind) .. ' ' .. tostring(entry.dig_id)) return end local apiReason = type(data) == 'table' and tostring(data.reason or data.error or '') or tostring(reason or '') if status == 400 or status == 403 or status == 404 or status == 410 or status == 422 then for index, queued in ipairs(khApiQueue) do if queued.kind == entry.kind and tostring(queued.dig_id):lower() == tostring(entry.dig_id):lower() then khApiQueueMoveToDead(index, status, data, apiReason) break end end khApiRequestBusy = false khApiNextAttemptAt = os.clock() + 0.15 else khApiScheduleRetry(entry, status, data, apiReason ~= '' and apiReason or 'http') end end) if not okRun then khApiScheduleRetry(entry, 0, nil, runError) end end) if not okStart then khApiScheduleRetry(entry, 0, nil, 'worker start') return false end return true end function khApiQueueLootForCapture(capture, prizes) if type(capture) ~= 'table' or not khApiIsUuid(capture.digId) or type(prizes) ~= 'table' then return false end local items = {} for _, prize in pairs(prizes) do local name = khCleanDropItemName(prize and prize.name or '') if name ~= '' and #items < 40 then table.insert(items, {name = name, quantity = math.max(1, math.floor(tonumber(prize.count) or 1))}) end end if #items == 0 then return false end local entry = khApiQueueFind(capture.digId, 'loot') if entry ~= nil then entry.items = items entry.next_attempt_at = math.max(tonumber(entry.next_attempt_at) or 0, os.clock() + 1.0) khApiSaveQueue() return true end return khApiQueuePush({kind = 'loot', dig_id = capture.digId, event_id = capture.eventId, nickname = khApiGetNickname(), items = items, attempts = 0, next_attempt_at = os.clock() + 1.0}) end function khApiTick(nowClock) local now = tonumber(nowClock) or os.clock() khApiLoadQueue() khExpireMainDigCapture(now) if khApiRequestBusy or now < (tonumber(khApiNextAttemptAt) or 0) then return end if khApiSessionToken == '' or os.time() >= (tonumber(khApiSessionExpiresAt) or 0) - 30 then khApiStartSession() elseif #khApiQueue > 0 then khApiSendQueuedDig() end end function khReportDig(metadata) return khApiQueueTreasureReport(metadata) end function khQueueTreasureReportOnce(metadata) local capture = khGetMainDigCaptureForReport() if capture == nil then return false end return khApiQueueTreasureReport(capture) end function khFinalizeDropItems(items, dropType, metadata) if type(items) ~= 'table' then items = {} end local normalized = {} for _, source in ipairs(items) do local name = khCleanDropItemName(source and source.name or '') if name ~= '' then local count = math.max(1, math.floor((tonumber(source and source.count) or 1) + 0.5)) table.insert(normalized, {name = name, count = count}) end end if #normalized == 0 and dropType ~= 'chest' then table.insert(normalized, {name = 'Неизвестно', count = 1}) end if #normalized == 0 then return false end metadata = type(metadata) == 'table' and metadata or {} local timestamp = tonumber(metadata.timestamp) or os.time() local log = { id = khNextDropLogId(), timestamp = timestamp, date = metadata.date or os.date('%Y-%m-%d', timestamp), time = metadata.time or os.date('%H:%M:%S', timestamp), items = normalized, type = dropType or 'treasure' } table.insert(khDropLogs, log) khInvalidateDropProfitCache() khCurrentDropLogId = log.id khDropSelectedDate = log.date khDropSelectedLogId = log.id khPendingDropSession = nil khSaveDropStats() if log.type == 'chest' then nakhodkaNotify(sName .. 'Шкатулка обработана. Дроп сохранён в статистику.', -1, 'success', 3) khTyanCelebrate('dop') else nakhodkaNotify(sName .. 'Клад вскопан полностью', -1, 'success', 3) khTyanCelebrate('klad') end if log.type == 'treasure' then local capture = khGetMainDigCaptureForReport() if capture ~= nil then khApiQueueLootForCapture(capture, normalized) end end return true end function khFinalizePendingDropSession() if khPendingDropSession == nil then return false end local session = khPendingDropSession local items = session.items or {} if #items == 0 then for _, prize in pairs(khCefDropPrizeList or {}) do if type(prize) == 'table' and prize.name then table.insert(items, {name = prize.name, count = prize.count}) end end end return khFinalizeDropItems(items, 'treasure', session) end function khUpdatePendingDropSession(nowClock) nowClock = tonumber(nowClock) or os.clock() khExpireMainDigCapture(nowClock) if khPendingDropSession == nil then return end local age = nowClock - (tonumber(khPendingDropSession.lastDropClock) or nowClock) if not khCefDropDigSucceeded and age >= 35.0 then khPendingDropSession = nil end end function khHandlePreDigServerMessage(message) local text = clearNotifyText(message) local myNick = khGetMyNickname and khGetMyNickname() or '' local lower = khLowerCp1251(text) local lowerNick = khLowerCp1251(myNick) if lowerNick == '' or not lower:find(lowerNick, 1, true) then return false end local startedWord = string.char(237, 224, 247, 224, 235) -- nachal local digWord = string.char(234, 238, 239, 224, 242, 252) -- kopat if not lower:find(startedWord, 1, true) or not lower:find(digWord, 1, true) then return false end khCaptureDropDigLocation() khMarkDropTreasureContext(60.0) khStartDigCursorLock() return true end function khHandleDropServerMessage(message) if khIsOwnTreasureDugMessage(message) then khMarkDropTreasureContext(60.0) local session = khEnsurePendingDropSession() khCefDropDigSucceeded = true khStopDigCursorLock() return end if khIsOwnDopMessage(message) then khMarkDropTreasureContext(60.0) end end function khCefDropLocalText(text) text = tostring(text or '') if text == '' then return '' end text = khDropDecodeUtf8(text) return clearNotifyText(text) end function khCefDropHasTreasureText(text) local lower = khLowerCp1251(khCefDropLocalText(text)) local treasureWord = string.char(234, 235, 224, 228) -- klad return lower:find(treasureWord, 1, true) ~= nil end function khCefDropHasChestText(text) local lower = khLowerCp1251(khCefDropLocalText(text)) local chestWord = khLowerCp1251('шкатул') local rewardWord = khLowerCp1251('список наград') return lower:find(chestWord, 1, true) ~= nil or (lower:find(rewardWord, 1, true) ~= nil and lower:find(chestWord, 1, true) ~= nil) end function khCefDropLooksGarbageText(text) local value = khCefDropLocalText(text) if value:find('???', 1, true) then return true end local unknown = 0 for _ in value:gmatch('%?') do unknown = unknown + 1 end return unknown >= 3 and unknown >= math.floor(#value / 2) end function khCefDropOpenRewardView(data) khStopDigCursorLock() local view = nil local isEmpty = false if type(data) == 'table' then local first = data[1] if first == nil or (type(first) == 'string' and first:lower() == 'null') then isEmpty = true else view = tostring(first) end elseif type(data) == 'string' then local compact = data:gsub('%s+', ''):lower() if compact == 'null' or compact == '[]' or compact == '[null]' or compact == '[null,]' then isEmpty = true else view = data:match('%["?([%w%._%-]+)"?%]') or data end else isEmpty = true end if isEmpty then khCefDropCloseRewardView() return end local lower = tostring(view or ''):lower() if lower:find('mountain', 1, true) or lower:find('testdrive', 1, true) then khCefDropRewardMenuOpen = true khCefDropWaitingReopen = false khCefDropLastActionAt = os.clock() khCefDropTitleHasKlad = false else khCefDropCloseRewardView() end end function khCefDropCloseRewardView() khCefDropRewardMenuOpen = false khCefDropWaitingReopen = true khCefDropLastActionAt = os.clock() khCefDropTitleHasKlad = false end function khCefDropNormalizePrizeList(data) local list = nil if type(data) == 'table' then if type(data[1]) == 'table' and data[1][1] ~= nil then list = data[1] elseif type(data[1]) == 'table' and (data[1].id ~= nil or data[1].title ~= nil or data[1].name ~= nil) then list = data elseif type(data.data) == 'table' then list = khCefDropNormalizePrizeList(data.data) end elseif type(data) == 'string' and khJson ~= nil and type(khJson.decode) == 'function' then local ok, decoded = pcall(khJson.decode, data) if ok and type(decoded) == 'table' then list = khCefDropNormalizePrizeList(decoded) end end return list end function khCefDropRememberPrizeList(data) local list = khCefDropNormalizePrizeList(data) if type(list) ~= 'table' then return false end khCefDropPrizeList = {} for index, entry in ipairs(list) do if type(entry) == 'table' then local id = entry.id or entry.ID or entry.index or index local title = entry.title or entry.name or entry.label or entry.text or '' title = khCefDropLocalText(title) local count = tonumber(entry.count or entry.amount or entry.qty) local name = title if count == nil then local parsedName, parsedCount = khParseItemNameAndCount(title) name = parsedName or title count = parsedCount or 1 end name = khCleanDropItemName(name) count = math.max(1, math.floor((tonumber(count) or 1) + 0.5)) if name ~= '' then khCefDropPrizeList[tonumber(id) or id] = {name = name, count = count, raw = entry} end end end if khCefDropChestGarbageCandidate and os.clock() - (tonumber(khCefDropChestLastInitAt) or 0) < 3.5 then khCefDropChestActive = true khCefDropChestGarbageCandidate = false khCefDropChestItemMap = {} khCefDropChestPicks = {} khCefDropSessionActive = true end if khCefDropChestActive then for id, prize in pairs(khCefDropPrizeList) do khCefDropChestItemMap[tonumber(id) or id] = {name = prize.name, count = prize.count} end end if khCefDropTitleHasKlad then khCefDropSessionActive = true local capture = khGetMainDigCaptureForReport() if capture ~= nil then khApiQueueLootForCapture(capture, khCefDropPrizeList) end end return true end function khCefDropAddSelectedItem(fullName) fullName = khCleanDropItemName(khCefDropLocalText(fullName)) if fullName == '' then return false end if khCefDropPendingSelect == nil or not khCefDropRewardMenuOpen or not khCefDropTitleHasKlad then return false end local prize = khCefDropPrizeList and khCefDropPrizeList[khCefDropPendingSelect.id] or nil local count = tonumber(prize and prize.count) or 1 khCefDropSessionActive = true khCefDropDigSucceeded = true khMarkDropTreasureContext(60.0) khAddPendingDropItem(fullName, count) table.insert(khCefDropCurrentItems, {name = fullName, count = count}) khCefDropPendingSelect = nil khCefDropLastActionAt = os.clock() return true end function khHandleCefDropServerMessage(message) if khCefDropChestActive then return false end local fullName = khParseDropMessage(message) if fullName then return khCefDropAddSelectedItem(fullName) end return false end function khCefDropHandleInitializeText(data) local hasKlad = false local hasChest = false local hasGarbage = false local function scan(value) if value ~= nil then if khCefDropHasTreasureText(value) then hasKlad = true end if khCefDropHasChestText(value) then hasChest = true end if khCefDropLooksGarbageText(value) then hasGarbage = true end end end if type(data) == 'table' then scan(data.title); scan(data.desc); scan(data.name); scan(data.text) if type(data.data) == 'table' then scan(data.data.title); scan(data.data.desc); scan(data.data.name); scan(data.data.text) end for _, entry in ipairs(data) do if type(entry) == 'table' then scan(entry.title); scan(entry.desc); scan(entry.name); scan(entry.text) else scan(entry) end end else scan(data) end khCefDropTitleHasKlad = hasKlad if hasKlad then local confirmed = khConfirmMainDigCapture() -- Some CEF builds send addVehicles before the title. Preserve that -- already parsed list and bind it to the confirmed dig immediately. if confirmed and type(khCefDropPrizeList) == 'table' and next(khCefDropPrizeList) ~= nil then local capture = khGetMainDigCaptureForReport() if capture ~= nil then khApiQueueLootForCapture(capture, khCefDropPrizeList) end end khCaptureDopDigContext(false) end if hasChest then khCefDropChestActive = true khCefDropChestGarbageCandidate = false khCefDropChestLastInitAt = os.clock() khCefDropChestItemMap = {} khCefDropChestPicks = {} khCefDropSessionActive = true elseif hasGarbage and not hasKlad then khCefDropChestGarbageCandidate = true khCefDropChestLastInitAt = os.clock() else khCefDropChestActive = false khCefDropChestGarbageCandidate = false end if hasKlad then khCefDropSessionActive = true khMarkDropTreasureContext(60.0) end end function khCefDropHandleSelectVehicle(data) local id = nil if type(data) == 'table' then id = tonumber(data.id or data[1]) else id = tonumber(data) end if id == nil then return end if khCefDropChestActive then local prize = khCefDropChestItemMap and khCefDropChestItemMap[id] or nil if prize ~= nil then table.insert(khCefDropChestPicks, {name = prize.name, count = prize.count}) khCefDropSessionActive = true khCefDropLastActionAt = os.clock() end elseif khCefDropRewardMenuOpen and khCefDropTitleHasKlad then khCefDropPendingSelect = {id = id, t = os.clock()} khCefDropLastActionAt = os.clock() end end function khCefDataMentionsExcavations(value, depth) depth = tonumber(depth) or 0 if depth > 4 then return false end if type(value) == 'string' then local lower = value:lower() return lower:find('excavat', 1, true) ~= nil or lower:find('digging', 1, true) ~= nil end if type(value) == 'table' then for key, entry in pairs(value) do if khCefDataMentionsExcavations(key, depth + 1) or khCefDataMentionsExcavations(entry, depth + 1) then return true end end end return false end function khHandleExcavationsOpened() if khIsScriptEnabled() and not khIsNoTreasureServer() then khCaptureDopDigContext(true) khStartDigCursorLock() khUpdateDigCursorLock(os.clock()) end end function khRegisterCefDropHandlers() if khCef == nil or type(khCef.on) ~= 'function' then return false end khCef.on('event.setActiveView', function(event) local data = event and event.data or nil if khCefDataMentionsExcavations(data) then khHandleExcavationsOpened() else khCefDropOpenRewardView(data) khCefDropExpectCloseByView = false khCefDropExpectCloseDeadline = 0 end end) local excavationOpenEvents = { 'excavations.open', 'excavations.initialize', 'excavations.show', 'event.excavations.open', 'event.excavations.initialize', 'event.excavations.show' } for _, eventName in ipairs(excavationOpenEvents) do khCef.on(eventName, function() khHandleExcavationsOpened() end) end khCef.on('mountain.testDrive.close', function() khCefDropExpectCloseByView = true khCefDropExpectCloseDeadline = os.clock() + 1.0 end) khCef.on('event.mountain.testDrive.close', function() khCefDropCloseRewardView() end) khCef.on('excavations.clickOnTrigger', function() khCaptureDropDigLocation() khHandleExcavationsOpened() end) khCef.on('excavations.successfulSpaceClick', function() khArmMainDigCapture() khStopDigCursorLock() khCefDopSuccessfulSpaceClick() end) khCef.on('event.mountain.testDrive.initializeText', function(event) khCefDropHandleInitializeText(event and event.data or nil) end) khCef.on('event.mountain.testDrive.addVehicles', function(event) khCefDropRememberPrizeList(event and event.data or nil) end) khCef.on('mountain.testDrive.selectVehicle', function(event) khCefDropHandleSelectVehicle(event and event.data or nil) end) return true end function khUpdateCefDropSession(nowClock) nowClock = tonumber(nowClock) or os.clock() if khCefDropExpectCloseByView and khCefDropExpectCloseDeadline < nowClock then khCefDropCloseRewardView() khCefDropExpectCloseByView = false khCefDropExpectCloseDeadline = 0 end if khCefDropPendingSelect ~= nil and nowClock - (tonumber(khCefDropPendingSelect.t) or nowClock) > 3.0 then khCefDropPendingSelect = nil end if khCefDropWaitingReopen and nowClock - (tonumber(khCefDropLastActionAt) or nowClock) > 4.0 then if khCefDropSessionActive then if khCefDropChestActive then khFinalizeDropItems(khCefDropChestPicks, 'chest') elseif khCefDropDigSucceeded and khPendingDropSession ~= nil then khFinalizePendingDropSession() end end khCefDropWaitingReopen = false khCefDropPrizeList = {} khCefDropPendingSelect = nil khCefDropCurrentItems = {} khCefDropChestItemMap = {} khCefDropChestPicks = {} khCefDropChestActive = false khCefDropChestGarbageCandidate = false khCefDropSessionActive = false khCefDropTitleHasKlad = false khCefDropDigSucceeded = false end end pcall(khRegisterCefDropHandlers) local khTabs = { { icon = 0xF8E2, name = 'Основное', title = 'Основное', desc = 'Главные настройки скрипта.' }, { icon = 0xF15B, name = '3D маркеры', title = '3D маркеры', desc = 'Отображение основных точек и допок прямо в мире.', feature = 'markers3d' }, { icon = 0xF4D0, name = 'Команда', title = 'Команда', desc = 'Команда, союзники, зоны, SOS и чат.', feature = 'team' }, { icon = 0xF3E8, name = 'Точки', title = 'Точки', desc = 'Список всех основных точек: ближайшие сверху, клик по строке ставит или снимает метку.' }, { icon = 0xF2EE, name = 'Статистика', title = 'Дроп по дням', desc = 'Выбирай день, потом клад, справа будет список выпавших предметов и прибыль по заданным ценам.' }, { icon = 0xF632, name = 'Цены', title = 'Цены', desc = 'Укажи цену предметов, которые падают с кладов. По ним считается прибыль в статистике и панели Статистика.' }, { icon = 0xF3E6, name = 'Допки', title = 'Окно допок', desc = 'Список дополнительных кладов: сортировка по дистанции, выбор допки и постановка метки.', feature = 'dopki' }, { icon = 0xF3E5, name = 'Настройки', title = 'Настройки', desc = '' }, { icon = 0xF431, name = 'Информация', title = 'Информация', desc = 'Что за скрипт и какие команды сейчас доступны.' } } function khIsTabAvailable(tab) return tab == nil or tab.feature == nil or khFeatureEnabled(tab.feature) end function khVisibleTabCount() local count = 0 for _, tab in ipairs(khTabs) do if khIsTabAvailable(tab) then count = count + 1 end end return math.max(1, count) end function khVisibleTabsFrom(index) local count = 0 for i = index, #khTabs do if khIsTabAvailable(khTabs[i]) then count = count + 1 end end return math.max(1, count) end 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 = 9, cmd = '/khunload /khunl', desc = 'Выгрузить Nakhodka до перезапуска или ручной загрузки скрипта.' }, { 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 = 3, cmd = '/khteam', desc = 'Открыть окно команды.' }, { tab = 3, cmd = '/khinvite Nick/ID', desc = 'Пригласить игрока по нику или ID на твоем Arizona-сервере.' }, { tab = 3, cmd = '/khaccept Nick', desc = 'Принять приглашение от игрока.' }, { tab = 3, cmd = '/khdeny Nick', desc = 'Отклонить приглашение от игрока.' }, { tab = 3, cmd = '/khleave', desc = 'Выйти из команды.' }, { tab = 3, cmd = '/khkick Nick/ID', desc = 'Удалить союзника из команды по нику или ID, если ты лидер.' }, { tab = 3, cmd = '/kht текст', desc = 'Написать сообщение только участникам твоей команды.' }, { tab = 3, cmd = '/sos', desc = 'Позвать союзников: им поставится красный чекпоинт, GPS и уведомление. КД 15 секунд.' }, { tab = 3, cmd = '/sos off', desc = 'Отменить свой активный SOS-сигнал.' }, { 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' }, { tab = 4, cmd = '/restoregangzones /rgz', desc = '\xc2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xe8\xf2\xfc\x20\xf3\xe4\xe0\xeb\xb8\xed\xed\xfb\xe5\x20\xe7\xee\xed\xfb\x20\xe3\xe5\xf2\xf2\xee\x20\xe8\x20\xf1\xe5\xec\xe5\xe9\x2e' } } 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 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), leader = khUtf8Codepoint(0xF7BF) } 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 khGetCustomCompanionsPath() return khGetConfigDir() .. '\\nakhodka_companions.json' end function khGetCompanionResourceDir() local dir = khGetResourceDir() .. '\\companions' if type(doesDirectoryExist) ~= 'function' or not doesDirectoryExist(dir) then if type(createDirectory) == 'function' then pcall(createDirectory, dir) end end return dir end function khCustomJsonEscape(value) local text = tostring(value or '') text = text:gsub('\\', '\\\\'):gsub('"', '\\"'):gsub('\r', '\\r'):gsub('\n', '\\n'):gsub('\t', '\\t') text = text:gsub(string.char(8), '\\b'):gsub(string.char(12), '\\f') return '"' .. text .. '"' end function khCustomJsonText(value) local text = tostring(value or '') local ok, encoded = pcall(function() return u8(text) end) if ok and encoded ~= nil then text = tostring(encoded) end return khCustomJsonEscape(text) end function khCustomBufferText(buffer, maxLen, preserveLines) if buffer == nil or ffi == nil then return '' end local ok, text = pcall(ffi.string, buffer) if not ok or text == nil then return '' end local decoded = khPhraseJsonText(text) if preserveLines then text = tostring(decoded or ''):gsub('\r', ''):gsub('[\t]', ' ') if #text > (maxLen or 256) then text = text:sub(1, maxLen or 256) end else text = khTeamCleanText(decoded, maxLen or 256) end return text end function khCustomSetBuffer(buffer, value, maxLen) if buffer == nil or ffi == nil then return end local encoded = u8(tostring(value or '')) pcall(ffi.fill, buffer, maxLen, 0) if encoded ~= nil and encoded ~= '' then pcall(ffi.copy, buffer, tostring(encoded):sub(1, math.max(0, maxLen - 1))) end end function khCustomPhraseList(value, fallback) local result = {} local text = tostring(value or ''):gsub('\r', '') for line in (text .. '\n'):gmatch('(.-)\n') do line = khTeamCleanText(line, 220) if line ~= '' then result[#result + 1] = line end end if #result == 0 and fallback ~= nil then result[1] = fallback end return result end function khCustomPhraseText(list) local result = {} if type(list) == 'table' then for _, phrase in ipairs(list) do local text = khTeamCleanText(phrase, 220) if text ~= '' then result[#result + 1] = text end end end return table.concat(result, '\n') end function khCustomTemplatePhrases() return {'Фраза 1', 'Фраза 2', 'Фраза 3'} end function khCustomClampSize(value) value = math.floor((tonumber(value) or 260) + 0.5) if value < 200 then value = 200 end if value > 800 then value = 800 end return value end function khCustomCopyFile(source, target) local input = io.open(tostring(source or ''), 'rb') if not input then return false end local data = input:read('*a') input:close() if data == nil or #data == 0 or #data > 12 * 1024 * 1024 then return false end local output = io.open(tostring(target or ''), 'wb') if not output then return false end output:write(data) output:close() return true end function khCustomFind(id) id = tostring(id or '') for _, companion in ipairs(khCustomCompanions or {}) do if tostring(companion.id or '') == id then return companion end end return nil end function khCustomSyncGlobals() local companion = khCustomFind(khCustomActiveId) if companion == nil then return end companion.x = math.floor((tonumber(khTyanPos.x) or 240) + 0.5) companion.y = math.floor((tonumber(khTyanPos.y) or 500) + 0.5) companion.size = math.floor((tonumber(khTyanSize) or 136) + 0.5) end function khCustomLoadGlobals(companion) if companion == nil then return end khTyanPos.x = tonumber(companion.x) or 240 khTyanPos.y = tonumber(companion.y) or 500 khTyanSize = khCustomClampSize(companion.size) if khTyanSizeInput ~= nil then khTyanSizeInput[0] = khTyanSize end end function khCustomLoadEditor(companion) khCustomEditorId = companion ~= nil and tostring(companion.id or '') or nil if companion == nil then return end khCustomSetBuffer(khCustomNameInput, companion.name or '', 96) khCustomSetBuffer(khCustomHelloInput, companion.hello or 'Фраза', 256) khCustomSetBuffer(khCustomMoveInput, companion.move or 'Фраза', 256) khCustomSetBuffer(khCustomSavedInput, companion.saved or 'Фраза', 256) khCustomSetBuffer(khCustomPhrasesInput, khCustomPhraseText(companion.phrases), 16384) khCustomSetBuffer(khCustomCelebrateInput, khCustomPhraseText(companion.celebrate), 16384) khCustomSetBuffer(khCustomImageInput, companion.image or '', 320) end function khSaveCustomCompanions() local file = io.open(khGetCustomCompanionsPath(), 'wb') if not file then return false end local items = {} for _, companion in ipairs(khCustomCompanions or {}) do local phrases, celebrate = {}, {} for _, phrase in ipairs(companion.phrases or {}) do phrases[#phrases + 1] = khCustomJsonText(phrase) end for _, phrase in ipairs(companion.celebrate or {}) do celebrate[#celebrate + 1] = khCustomJsonText(phrase) end items[#items + 1] = '{' .. '"id":' .. khCustomJsonText(companion.id) .. ',' .. '"name":' .. khCustomJsonText(companion.name) .. ',' .. '"image":' .. khCustomJsonText(companion.image) .. ',' .. '"hello":' .. khCustomJsonText(companion.hello) .. ',' .. '"move":' .. khCustomJsonText(companion.move) .. ',' .. '"saved":' .. khCustomJsonText(companion.saved) .. ',' .. '"x":' .. tostring(math.floor(tonumber(companion.x) or 240)) .. ',' .. '"y":' .. tostring(math.floor(tonumber(companion.y) or 500)) .. ',' .. '"size":' .. tostring(math.floor(tonumber(companion.size) or 136)) .. ',' .. '"phrases":[' .. table.concat(phrases, ',') .. '],' .. '"celebrate":[' .. table.concat(celebrate, ',') .. ']}' end file:write('{"version":1,"companions":[' .. table.concat(items, ',') .. ']}' .. string.char(10)) file:close() return true end function khLoadCustomCompanions() khCustomCompanions = {} local text = khReadTextFile(khGetCustomCompanionsPath()) if text == nil or text == '' then khSaveCustomCompanions() return end local data = khJsonDecodeConfig(text) if type(data) ~= 'table' or type(data.companions) ~= 'table' then return end for _, item in ipairs(data.companions) do local id = khTeamCleanText(item.id or '', 48):gsub('[^%w_%-]', '') if id ~= '' then local companion = { id = id, name = khPhraseJsonText(item.name or 'Мой спутник'), image = khTeamCleanText(item.image or '', 120), hello = khPhraseJsonText(item.hello or 'Фраза'), move = khPhraseJsonText(item.move or 'Фраза'), saved = khPhraseJsonText(item.saved or 'Фраза'), x = tonumber(item.x) or 240, y = tonumber(item.y) or 500, size = khCustomClampSize(item.size), phrases = khPhraseListFromJson(item.phrases), celebrate = khPhraseListFromJson(item.celebrate) } if companion.hello == 'Привет!' then companion.hello = 'Фраза' end if companion.move == 'Я рядом, всё хорошо.' or companion.move == 'Я рядом.' then companion.move = 'Фраза' end if companion.saved == 'Готово, я останусь здесь.' or companion.saved == '' then companion.saved = 'Фраза' end if #companion.phrases == 0 or (#companion.phrases == 1 and (companion.phrases[1] == 'Я рядом, всё получится.' or companion.phrases[1] == 'Всё получится.')) then companion.phrases = khCustomTemplatePhrases() end if #companion.celebrate == 0 or (#companion.celebrate == 1 and (companion.celebrate[1] == 'Ты молодец, так держать.' or companion.celebrate[1] == 'Так держать!')) then companion.celebrate = khCustomTemplatePhrases() end if companion.image ~= '' then companion.imagePath = khGetCompanionResourceDir() .. '\\' .. companion.image end khCustomCompanions[#khCustomCompanions + 1] = companion end end end function khCustomChooseImage() if khComdlg32 == nil or ffi == nil then return nil end local buffer = ffi.new('char[512]') local ofn = ffi.new('NK_OPENFILENAMEA') ofn.lStructSize = ffi.sizeof(ofn) ofn.lpstrFilter = 'PNG/JPEG images\0*.png;*.jpg;*.jpeg\0All files\0*.*\0\0' ofn.lpstrFile = buffer ofn.nMaxFile = 512 ofn.lpstrTitle = 'Выбери картинку спутника' ofn.Flags = 0x00001000 + 0x00000800 local ok, picked = pcall(function() return khComdlg32.GetOpenFileNameA(ofn) end) if not ok or tonumber(picked) == 0 then return nil end local source = ffi.string(buffer) if source == nil or source == '' then return nil end local extension = source:match('(%.[^%.\\/]+)$') extension = extension and extension:lower() or '' if extension ~= '.png' and extension ~= '.jpg' and extension ~= '.jpeg' then return nil end local companion = khCustomFind(khCustomActiveId) if companion == nil then return nil end local fileName = companion.id .. extension local target = khGetCompanionResourceDir() .. '\\' .. fileName if not khCustomCopyFile(source, target) then return nil end companion.image = fileName companion.imagePath = target khCustomSetBuffer(khCustomImageInput, fileName, 320) khTyanTextures[companion.id] = nil khTyanTextureTriedByVariant[companion.id] = nil khSaveCustomCompanions() return fileName end function khCustomCreate() khCustomCreateSerial = khCustomCreateSerial + 1 local id = 'custom_' .. tostring(os.time()) .. '_' .. tostring(khCustomCreateSerial) local companion = { id = id, name = 'Мой спутник', image = '', hello = 'Фраза', move = 'Фраза', saved = 'Фраза', x = 240, y = 500, size = 136, phrases = khCustomTemplatePhrases(), celebrate = khCustomTemplatePhrases() } khCustomCompanions[#khCustomCompanions + 1] = companion khSaveCustomCompanions() khCustomSelect(id) return companion end function khCustomSelect(id) khCustomSyncGlobals() local companion = khCustomFind(id) if companion == nil then return false end khCustomActiveId = companion.id khCustomSelectedId = companion.id khTyanVariant = 0 khCustomLoadGlobals(companion) khCustomLoadEditor(companion) khTyanText = '' khTyanWasEnabled = false khTyanScheduleNext(os.clock() + 0.6) if type(khSaveMainSettings) == 'function' then khSaveMainSettings() end return true end function khCustomCommitEditor() local companion = khCustomFind(khCustomSelectedId) if companion == nil then return false end local name = khCustomBufferText(khCustomNameInput, 64) companion.name = name ~= '' and name or 'Мой спутник' companion.hello = khCustomBufferText(khCustomHelloInput, 180) if companion.hello == '' then companion.hello = 'Фраза' end companion.move = khCustomBufferText(khCustomMoveInput, 180) if companion.move == '' then companion.move = 'Фраза' end companion.saved = khCustomBufferText(khCustomSavedInput, 180) if companion.saved == '' then companion.saved = 'Фраза' end companion.phrases = khCustomPhraseList(khCustomBufferText(khCustomPhrasesInput, 16000, true), nil) if #companion.phrases == 0 then companion.phrases = khCustomTemplatePhrases() end companion.celebrate = khCustomPhraseList(khCustomBufferText(khCustomCelebrateInput, 16000, true), nil) if #companion.celebrate == 0 then companion.celebrate = khCustomTemplatePhrases() end khSaveCustomCompanions() return true end function khCustomDelete(id) local target = khCustomFind(id) if target == nil then return false end if khCustomActiveId == target.id then khCustomSyncGlobals() khCustomActiveId = nil khCustomSelectedId = nil khCustomRestoreBuiltInGlobals() end for index, companion in ipairs(khCustomCompanions) do if companion.id == target.id then table.remove(khCustomCompanions, index); break end end if target.imagePath ~= nil then pcall(os.remove, target.imagePath) end khSaveCustomCompanions() return true end function khTyanCurrentPhrases() local custom = khCustomFind(khCustomActiveId) if custom ~= nil then return custom.phrases end return khTyanVariant == 1 and khMaratPhrases or khTyanPhrases end function khTyanCurrentCelebratePhrases() local custom = khCustomFind(khCustomActiveId) if custom ~= nil then return custom.celebrate end return khTyanVariant == 1 and khMaratCelebratePhrases or khTyanCelebratePhrases end function khTyanHelloPhrase() local custom = khCustomFind(khCustomActiveId) if custom ~= nil then return custom.hello end return khTyanVariant == 1 and 'Спасибо хорошо' or 'Привет =)' end function khTyanMoveStartPhrase() local custom = khCustomFind(khCustomActiveId) if custom ~= nil then return custom.move end return khTyanVariant == 1 and 'Передвинь меня куда удобно, я постою рядом.' or 'Аккуратно передвинь меня, я буду рядом.' end function khTyanMoveSavedPhrase() local custom = khCustomFind(khCustomActiveId) if custom ~= nil then return custom.saved or 'Фраза' end return khTyanVariant == 1 and 'Нормально, тут удобно стоять.' or 'Готово, я тихонько останусь здесь.' end function khTyanMoveSavedNotify() local custom = khCustomFind(khCustomActiveId) if custom ~= nil then return 'Позиция спутника сохранена.' end return khTyanVariant == 1 and 'Позиция Марата сохранена.' or 'Позиция тяночки сохранена.' end function khCustomRestoreBuiltInGlobals() local cfg = mainCfg ~= nil and mainCfg.Tyan or nil khTyanPos.x = tonumber(cfg and cfg.x) or 240 khTyanPos.y = tonumber(cfg and cfg.y) or 500 khTyanSize = khCustomClampSize(cfg and cfg.size) if khTyanSizeInput ~= nil then khTyanSizeInput[0] = khTyanSize end end function khTyanApplyVariantNow(value, nowClock) local nextVariant = tonumber(value) == 1 and 1 or 0 local hadCustom = khCustomActiveId ~= nil if hadCustom then khCustomSyncGlobals(); khCustomActiveId = nil; khCustomSelectedId = nil; khCustomRestoreBuiltInGlobals() end if khTyanVariant ~= nextVariant then khTyanVariant = nextVariant khTyanText = '' khTyanWasEnabled = false khTyanScheduleNext((tonumber(nowClock) or os.clock()) + 0.6) elseif hadCustom then 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 khCustomActiveId == nil and (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 not khFeatureEnabled('companion') or 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 custom = khCustomFind(khCustomActiveId) if custom ~= nil and custom.imagePath ~= nil and custom.imagePath ~= '' then return custom.imagePath end 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 local textureKey = khCustomActiveId ~= nil and tostring(khCustomActiveId) or variant if khTyanTextures ~= nil and khTyanTextures[textureKey] ~= nil then khTyanTexture = khTyanTextures[textureKey] khTyanTextureVariant = variant return khTyanTextures[textureKey] end if khTyanTextureTriedByVariant ~= nil and khTyanTextureTriedByVariant[textureKey] then return nil end if khTyanTextureTriedByVariant == nil then khTyanTextureTriedByVariant = {} end if khTyanTextures == nil then khTyanTextures = {} end khTyanTextureTriedByVariant[textureKey] = 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[textureKey] = tex khTyanTexture = tex khTyanTextureVariant = variant end end return khTyanTextures[textureKey] 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 not khFeatureEnabled('companion') then khTyanWasEnabled = false; return end if khTyanEnabled == nil or not khTyanEnabled[0] then khTyanWasEnabled = false return end local activeCustom = khCustomFind(khCustomActiveId) if activeCustom ~= nil and tostring(activeCustom.image or '') == '' then khTyanText = '' khTyanLoadingDots = false 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 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 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.TitleBg, khThemeColor(theme, 'titleBg')) imgui.PushStyleColor(imgui.Col.TitleBgActive, khThemeColor(theme, 'titleBgActive')) imgui.PushStyleColor(imgui.Col.TitleBgCollapsed, khThemeColor(theme, 'titleBgCollapsed')) 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 function khPopStyle() imgui.PopStyleColor(23) imgui.PopStyleVar(5) end 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 totalTabs = khVisibleTabCount() local remaining = khVisibleTabsFrom(index) 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 maxHeight = totalTabs <= 5 and 76 or (totalTabs <= 7 and 66 or 58) local minHeight = totalTabs <= 5 and 52 or 38 local height = math.max(minHeight, math.min(maxHeight, (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 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 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 khRenderTeamChatPanel(accent, muted, green) imgui.TextColored(accent, khText('Состав команды')) imgui.SameLine() if imgui.Button(khText('Скрыть') .. '##kh_team_chat_hide', imgui.ImVec2(82, 24)) then khTeamChatVisible = false end imgui.Separator() local logHeight = math.max(180, imgui.GetContentRegionAvail().y - 108) imgui.BeginChild('##kh_team_chat_log', imgui.ImVec2(0, logHeight), true) if #khTeamChatLogs == 0 then imgui.TextWrapped(khText('Сообщений пока нет. Напиши через /kht текст или поле ниже.')) else for _, msg in ipairs(khTeamChatLogs) do local from = khTeamCleanText(msg.from or 'team', 32) local text = khTeamCleanText(msg.text or '', 480) imgui.TextColored(green, khText(from .. ':')) imgui.SameLine() imgui.TextWrapped(khText(text)) end if khTeamChatNeedsScroll and imgui.SetScrollHereY ~= nil then imgui.SetScrollHereY(1.0) khTeamChatNeedsScroll = false end end imgui.EndChild() if khTeamEmojiOpen then imgui.BeginChild('##kh_team_emoji_row', imgui.ImVec2(0, 30), false) for i, token in ipairs(khTeamEmojiTokens or {}) do if i > 1 then imgui.SameLine(0, 5) end if imgui.Button(token .. '##kh_emoji_' .. tostring(i), imgui.ImVec2(52, 24)) then local current = khTeamBufferText(khTeamChatInput, 480) if current ~= '' then current = current .. ' ' end khTeamSetBuffer(khTeamChatInput, current .. token, 1024) end end imgui.EndChild() end local inputFlags = 0 if imgui.InputTextFlags ~= nil and imgui.InputTextFlags.EnterReturnsTrue ~= nil then inputFlags = imgui.InputTextFlags.EnterReturnsTrue end imgui.PushItemWidth(math.max(120, imgui.GetContentRegionAvail().x - 156)) local enterPressed = false if khTeamChatInput ~= nil then enterPressed = imgui.InputText('##kh_team_chat_input', khTeamChatInput, 1024, inputFlags) end imgui.PopItemWidth() imgui.SameLine() if imgui.Button(khText('Смайлы') .. '##kh_team_emoji_toggle', imgui.ImVec2(68, 28)) then khTeamEmojiOpen = not khTeamEmojiOpen end imgui.SameLine() local sendPressed = imgui.Button(khText('Отпр.') .. '##kh_team_chat_send', imgui.ImVec2(68, 28)) if enterPressed or sendPressed then local text = khTeamBufferText(khTeamChatInput, 480) khTeamSendChat(text) khTeamSetBuffer(khTeamChatInput, '', 1024) end imgui.TextColored(muted, khText('Команда: /kht текст. Сообщения видны только участникам твоей команды.')) end function khRenderTeamWindow() local accent = khThemeColor(nil, 'accentText') if not khFeatureEnabled('team') then imgui.TextWrapped(khText('Командный модуль отключен в этой сборке Nakhodka. Собери версию с командой на сайте, если она разрешена на твоем сервере.')) return end local green = imgui.ImVec4(0.35, 0.95, 0.48, 1.00) local red = imgui.ImVec4(1.00, 0.32, 0.38, 1.00) local yellow = imgui.ImVec4(1.00, 0.78, 0.28, 1.00) local muted = khThemeColor(nil, 'muted') 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 statusColor = muted if khTeam.status == 'online' then statusColor = green elseif khTeam.status == 'error' then statusColor = red elseif khTeam.status == 'warning' then statusColor = yellow end local function fullButton(label, id, height) return imgui.Button(khText(label) .. id, imgui.ImVec2(-1, height or 30)) end local function halfButtons(leftLabel, leftId, rightLabel, rightId) local avail = imgui.GetContentRegionAvail() local width = math.max(86, (avail.x - 8) / 2) local leftPressed = imgui.Button(khText(leftLabel) .. leftId, imgui.ImVec2(width, 30)) imgui.SameLine() local rightPressed = imgui.Button(khText(rightLabel) .. rightId, imgui.ImVec2(width, 30)) return leftPressed, rightPressed end local teamCount = 0 for _ in pairs(khTeamMembers) do teamCount = teamCount + 1 end if khTeamHasTeam() then teamCount = teamCount + 1 end local nearbyPlayers = khTeamBuildRosterList() local nearbyCount = #nearbyPlayers local visibleStatusText = khTeam.statusText or 'Не подключено' if khTeamEnabled[0] and visibleStatusText == 'Командный модуль выключен' then visibleStatusText = 'Подключение...' statusColor = yellow end imgui.BeginChild('##kh_team_root', imgui.ImVec2(0, 0), false) imgui.BeginChild('##kh_team_status', imgui.ImVec2(0, 176), true, noScrollFlags) if khToggle(khText('Включить команду'), khTeamEnabled) then khTeamSaveSettings() if khTeamEnabled[0] then khTeam.lastError = '' khTeamSetStatus('warning', 'Подключение...') khStartTeamClient() khTeamQueueHello() else khTeamSetStatus('offline', 'Командный модуль выключен') khTeam.lastError = '' khTeam.lastErrorAt = 0 khTeam.ownSosActive = false khTeam.ownSosUntil = 0 khTeam.ownSosX, khTeam.ownSosY, khTeam.ownSosZ = nil, nil, nil khTeamOutgoing = {} khTeamIncoming = {} khTeamInvites = {} khTeamMembers = {} khTeamRoster = {} khTeamRosterVisible = false khClearTeamMapArtifacts() end end khHelp('Включает командную связь: союзники, зоны, позиции, SOS и чат. Командные чат-команды работают только когда модуль включен.') imgui.SameLine() imgui.TextColored(statusColor, khText(visibleStatusText)) imgui.SameLine() imgui.TextColored(muted, khText('Сервер:')) imgui.SameLine() imgui.TextColored(accent, khText(khTeam.serverLabel or 'Не определен')) imgui.Separator() if khFeatureEnabled('teamMap') then local mapToggleChanged = khToggle(khText('Мини-карта'), khTeamMapEnabled) khHelp('Включает или выключает мини-карту команды. Клавишу можно выбрать кнопкой рядом.') imgui.SameLine() local mapKeyButton = khTeamMapKeyWaiting and 'Выбери клавишу' or ('Клавиша: ' .. khTeamMapKeyName(khTeamMapKey or 0x47)) if imgui.Button(khText(mapKeyButton) .. '##kh_team_map_key', imgui.ImVec2(155, 24)) then khUnloadKeyWaiting = false khTeamMapKeyWaiting = true nakhodkaNotify('Нажми клавишу или Mouse 4/5 для мини-карты. Esc - отмена.', -1, 'info', 3) end imgui.SameLine() local holdToggleChanged = khToggle(khText('Удерживать'), khTeamMapHoldMode) khHelp('Если включено, мини-карта показывается только пока клавиша зажата. Если выключено, клавиша открывает и закрывает карту.') imgui.SameLine() imgui.Text(khText('\xcf\xf0\xee\xe7\xf0\xe0\xf7\xed\xee\xf1\xf2\xfc')) imgui.SameLine() imgui.PushItemWidth(88) local transparencyChanged = imgui.SliderInt('##kh_team_map_transparency', khTeamMapTransparency, 0, 80, '%d%%') imgui.PopItemWidth() if transparencyChanged then khTeamSaveSettings() end if holdToggleChanged then if khTeamMapHoldMode ~= nil and khTeamMapHoldMode[0] then khTeamMapOpen = false end khTeamSaveSettings() end if mapToggleChanged then khTeamSaveSettings() end else khTeamMapClose() end if khSpawnFeatureEnabled ~= nil and khSpawnMapEnabled ~= nil then local spawnRowAvail = imgui.GetContentRegionAvail().x local spawnButtonWidth = 104 local spawnMiddleWidth = spawnButtonWidth + 16 local spawnLeftWidth = math.max(210, ((spawnRowAvail - spawnMiddleWidth) * 0.5) - 38) local spawnRightWidth = math.max(210, spawnRowAvail - spawnLeftWidth - spawnMiddleWidth) imgui.Columns(3, '##kh_spawn_modes', false) imgui.SetColumnWidth(0, spawnLeftWidth) imgui.SetColumnWidth(1, spawnMiddleWidth) imgui.SetColumnWidth(2, spawnRightWidth) local spawnMapChanged = khToggle(khText('Показывать спавны на миникарте'), khSpawnMapEnabled) khHelp('Показывает на миникарте все просканированные дома, трейлеры, вокзал и организации.') imgui.NextColumn() if imgui.Button(khText('Спавны') .. '##kh_spawns_open', imgui.ImVec2(spawnButtonWidth, 24)) then khSpawnWindowOpen[0] = true end imgui.NextColumn() local spawnNearestChanged = khToggle(khText('Ближайший спавн к зоне'), khSpawnFeatureEnabled) khHelp('Ищет ближайший к текущей зоне клада спавн и ставит на него метку на карте. Если он найден, на мини-карте останется только он.') imgui.Columns(1) if spawnMapChanged or spawnNearestChanged then khSaveMainSettings() khSpawnRefreshMapBlips(true) khSpawnRefreshNearestBlip(true) end end local nearbyVisibilityChanged = khToggle(khText('Показывать меня среди игроков'), khTeamShowNearby) khHelp('Если выключено, твой ник не показывается в списке «Люди рядом» у других игроков.') if nearbyVisibilityChanged then khTeamSaveSettings() if khTeamEnabled ~= nil and khTeamEnabled[0] then khTeamQueueHello() khTeamRequestRoster(true) end end imgui.TextColored(muted, khText('Быстрые команды: /sos, /kht, /khinvite, /khaccept, /khdeny, /khleave, /khkick.')) if khTeam.lastErrorAt ~= nil and tonumber(khTeam.lastErrorAt) ~= nil and tonumber(khTeam.lastErrorAt) > 0 and os.clock() - tonumber(khTeam.lastErrorAt) > 18.0 then khTeam.lastError = '' khTeam.lastErrorAt = 0 end local renderLastError = tostring(khTeam.lastError or '') if teamCount > 0 and renderLastError:find('не состоишь в команде', 1, true) then khTeam.lastError = '' khTeam.lastErrorAt = 0 renderLastError = '' end if renderLastError ~= '' then imgui.TextColored(red, khText('Последняя ошибка: ' .. renderLastError)) else imgui.TextColored(muted, khText('Игроки видят друг друга только на текущем Arizona-сервере. Сервер определяется автоматически.')) end imgui.EndChild() imgui.BeginChild('##kh_team_actions', imgui.ImVec2(326, 0), true) imgui.TextColored(accent, khText('Заявки')) imgui.Separator() imgui.Text(khText('Ник или ID игрока')) imgui.PushItemWidth(-1) if khTeamInviteInput ~= nil then imgui.InputText('##kh_team_invite_input', khTeamInviteInput, 32) end imgui.PopItemWidth() local nick = khTeamBufferText(khTeamInviteInput, 32) local invitePressed, kickPressed = halfButtons('Пригласить', '##kh_team_invite_btn', 'Кикнуть', '##kh_team_kick_btn') if invitePressed then khTeamCmdInvite(nick) end if kickPressed then khTeamCmdKick(nick) end if fullButton(khTeamRosterVisible and 'Скрыть людей рядом' or 'Люди рядом', '##kh_team_roster_toggle', 30) then khTeamRosterVisible = not khTeamRosterVisible if khTeamRosterVisible then khTeamRequestRoster(true) end end khHelp('Показывает всех игроков на этом сервере, у кого включен командный модуль Nakhodka. Инвайт разрешен только им.') if fullButton(khTeamChatVisible and 'Скрыть чат' or 'Чат команды', '##kh_team_chat_toggle', 30) then khTeamChatVisible = not khTeamChatVisible end imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0.55, 0.12, 0.18, 1.00)) imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(0.72, 0.18, 0.24, 1.00)) imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.46, 0.08, 0.14, 1.00)) local sosActive = khTeamHasTeam() and khTeam.ownSosActive and tonumber(khTeam.ownSosUntil or 0) > os.clock() if fullButton(sosActive and 'Отменить SOS' or 'SOS союзникам', '##kh_team_sos_btn', 34) then if sosActive then khTeamCmdCancelSos() else khTeamCmdSos() end end imgui.PopStyleColor(3) khHelp('То же самое, что /sos: союзникам приходит красная уведа, GPS и большой чекпоинт на твою позицию. Повторный SOS раз в 15 секунд.') if fullButton('Выйти из команды', '##kh_team_leave_btn', 30) then khTeamCmdLeave() end if fullButton('Переподключиться', '##kh_team_reconnect', 30) then khTeamCmdReconnect() end imgui.Spacing() imgui.TextColored(accent, khText('Заявки')) imgui.Separator() local hasInvite = false for key, invite in pairs(khTeamInvites) do if invite.expiresAt and invite.expiresAt > os.clock() then hasInvite = true imgui.Text(khText(invite.from)) local acceptPressed, denyPressed = halfButtons('Принять', '##kh_inv_accept_' .. key, 'Отклонить', '##kh_inv_deny_' .. key) if acceptPressed then khTeamCmdAccept(invite.from) end if denyPressed then khTeamCmdDeny(invite.from) end else khTeamInvites[key] = nil end end if not hasInvite then imgui.TextColored(muted, khText('Активных заявок нет.')) end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_team_members', imgui.ImVec2(0, 0), true) if khTeamChatVisible then khRenderTeamChatPanel(accent, muted, green) imgui.EndChild() imgui.EndChild() return end if khTeamRosterVisible then imgui.TextColored(accent, khText('Люди рядом')) imgui.SameLine() imgui.TextColored(muted, tostring(nearbyCount)) imgui.SameLine() if imgui.Button(khText('Обновить') .. '##kh_team_roster_refresh', imgui.ImVec2(88, 24)) then khTeamRequestRoster(true) end imgui.Separator() if not khTeam.connected then imgui.TextWrapped(khText('Нет подключения, список игроков пока недоступен.')) elseif nearbyCount == 0 then imgui.TextWrapped(khText('Пока никого не видно. Игрок должен быть на этом же сервере и включить команду в Nakhodka.')) else local rosterAvail = imgui.GetContentRegionAvail().x local rosterActionWidth = 126 local rosterIdWidth = 44 local rosterSeenWidth = 72 local rosterNickWidth = math.max(132, rosterAvail - rosterActionWidth - rosterIdWidth - rosterSeenWidth - 24) imgui.Columns(4, '##kh_team_roster_cols', false) imgui.SetColumnWidth(0, rosterNickWidth) imgui.SetColumnWidth(1, rosterIdWidth) imgui.SetColumnWidth(2, rosterSeenWidth) imgui.SetColumnWidth(3, rosterActionWidth) imgui.TextColored(accent, khText('Действия')) imgui.NextColumn() imgui.TextColored(accent, khText('ID')) imgui.NextColumn() imgui.TextColored(accent, khText('В сети')) imgui.NextColumn() imgui.TextColored(accent, khText('Действия')) imgui.NextColumn() imgui.Separator() imgui.PushStyleVarVec2(imgui.StyleVar.ItemSpacing, imgui.ImVec2(4, 4)) for index, player in ipairs(nearbyPlayers) do local nickName = khTeamDisplayNick(player, 32) local playerId = khTeamDisplayPlayerId(player) local seenText = khTeamLastSeenText(os.clock() - (tonumber(player.lastSeenAt or os.clock()) or os.clock())) imgui.Text(khText(nickName)) imgui.NextColumn() imgui.Text(khText(playerId ~= nil and tostring(playerId) or '-')) imgui.NextColumn() imgui.TextColored(green, khText(seenText)) imgui.NextColumn() if imgui.Button(khText('Инв.') .. '##kh_team_roster_inv_' .. tostring(index), imgui.ImVec2(58, 24)) then if playerId ~= nil then khTeamCmdInvite(tostring(playerId)) else khTeamCmdInvite(nickName) end end imgui.SameLine() if imgui.Button(khText('GPS') .. '##kh_team_roster_gps_' .. tostring(index), imgui.ImVec2(46, 24)) then local x, y, z = tonumber(player.x), tonumber(player.y), tonumber(player.z) or 0 if x and y then khTeamSetTargetMarker(x, y, z, nickName, 'gps') nakhodkaNotify('GPS на ' .. nickName .. ' поставлен.', -1, 'success', 2) end end imgui.NextColumn() end imgui.PopStyleVar() imgui.Columns(1) end imgui.Spacing() imgui.Separator() end imgui.TextColored(accent, khText('Состав команды')) imgui.SameLine() imgui.TextColored(muted, tostring(teamCount)) imgui.Separator() if teamCount == 0 then imgui.TextWrapped(khText('Союзников пока нет. Нажми "Люди рядом", выбери игрока с включенным Nakhodka и отправь приглашение.')) else imgui.Columns(5, '##kh_team_members_cols', false) local membersAvail = imgui.GetContentRegionAvail().x local actionWidth = 138 imgui.SetColumnWidth(0, math.max(150, membersAvail - actionWidth - 190)) imgui.SetColumnWidth(1, 78) imgui.SetColumnWidth(2, 44) imgui.SetColumnWidth(3, 38) imgui.SetColumnWidth(4, actionWidth) imgui.TextColored(accent, khText('Ник')) imgui.NextColumn() imgui.TextColored(accent, khText('Сеть')) imgui.NextColumn() imgui.TextColored(accent, khText('Зона')) imgui.NextColumn() imgui.TextColored(accent, khText('SOS')) imgui.NextColumn() imgui.TextColored(accent, khText('Действия')) imgui.NextColumn() imgui.Separator() local sortedMembers = {} if khTeamHasTeam() then table.insert(sortedMembers, { token = tostring(khTeam.token or 'self'), member = { nick = khTeamGetOwnNickname and khTeamGetOwnNickname() or 'Ты', playerId = khGetMyPlayerId(), online = true, isSelf = true, zone = khTeamOwnZone, sosUntil = khTeam.ownSosUntil } }) end for token, member in pairs(khTeamMembers) do table.insert(sortedMembers, {token = token, member = member}) end table.sort(sortedMembers, function(a, b) local am = a.member or {} local bm = b.member or {} if am.isSelf == true then return bm.isSelf ~= true end if bm.isSelf == true then return false end local ao = am.online ~= false local bo = bm.online ~= false if ao ~= bo then return ao end local aa = tonumber(am.lastSeenAgo) or 0 local ba = tonumber(bm.lastSeenAgo) or 0 if aa ~= ba then return aa < ba end local an = khTeamDisplayNick(am, 32):lower() local bn = khTeamDisplayNick(bm, 32):lower() return an < bn end) for _, row in ipairs(sortedMembers) do local token, member = row.token, row.member local nickName = khTeamDisplayNick(member, 32) local online = member.online ~= false local memberIdForName = khTeamDisplayPlayerId(member) local hasZone = type(member.zone) == 'table' and khTeamZoneIsFresh(member,token) local sosActive = member.isSelf == true and khTeam.ownSosActive and tonumber(khTeam.ownSosUntil or 0) > os.clock() or select(1, khTeamMemberSosState(member)) if tostring(token or '') == tostring(khTeam.leaderToken or '') and khBiIcons ~= nil and khBiIcons.leader ~= nil then imgui.TextColored(accent, khBiIcons.leader) imgui.SameLine(0, 4) end imgui.Text(khText(nickName)) imgui.NextColumn() imgui.TextColored(online and green or muted, khText(online and 'онлайн' or khTeamLastSeenText(member.lastSeenAgo))) imgui.NextColumn() imgui.TextColored(hasZone and green or muted, khText(hasZone and 'есть' or 'нет')) imgui.NextColumn() imgui.TextColored(sosActive and red or muted, khText(sosActive and 'SOS' or '-')) imgui.NextColumn() if member.isSelf == true then imgui.TextColored(muted, khText('Ты')) else if imgui.Button(khText('GPS') .. '##kh_team_gps_' .. tostring(token), imgui.ImVec2(48, 24)) then local x, y, z = tonumber(member.x), tonumber(member.y), tonumber(member.z) or 0 if x and y then khTeamSetTargetMarker(x, y, z, nickName, 'gps') if online then nakhodkaNotify('GPS на союзника ' .. nickName .. ' поставлен.', -1, 'success', 2) else nakhodkaNotify('Метка на последнее место выхода ' .. nickName .. ' поставлена.', -1, 'success', 3) end end end imgui.SameLine() if imgui.Button(khText('Кик') .. '##kh_team_kick_' .. tostring(token), imgui.ImVec2(58, 24)) then khTeamCmdKick(nickName) end end imgui.NextColumn() end imgui.Columns(1) imgui.Separator() imgui.TextColored(muted, khText('На карте видны позиции, зоны кладов союзников и SOS-сигналы.')) end imgui.EndChild() imgui.EndChild() end function khRenderCustomCompanionsWindow() if khCustomWindowOpen == nil or not khCustomWindowOpen[0] then return end local accent = khThemeColor(nil, 'accentText') local muted = khThemeColor(nil, 'muted') local flags = 0 if imgui.WindowFlags.NoCollapse ~= nil then flags = flags + imgui.WindowFlags.NoCollapse end if imgui.WindowFlags.NoSavedSettings ~= nil then flags = flags + imgui.WindowFlags.NoSavedSettings end imgui.SetNextWindowSize(imgui.ImVec2(620, 620), imgui.Cond.FirstUseEver or imgui.Cond.Always) if imgui.Begin(khText('Мои спутники') .. '##kh_custom_companions_window', khCustomWindowOpen, flags) then imgui.TextWrapped(khText('Создавай сколько угодно спутников. У каждого свои имя, картинка и списки фраз: одна фраза на строку.')) imgui.Separator() if imgui.Button(khText('Создать спутника') .. '##kh_custom_window_create', imgui.ImVec2(-1, 30)) then khCustomCreate() end imgui.BeginChild('##kh_custom_window_list', imgui.ImVec2(190, 0), true) imgui.TextColored(accent, khText('Список')) imgui.Separator() if #khCustomCompanions == 0 then imgui.TextWrapped(khText('Пока нет пользовательских спутников.')) else for _, companion in ipairs(khCustomCompanions) do local selected = tostring(khCustomSelectedId or '') == tostring(companion.id or '') if selected then imgui.PushStyleColor(imgui.Col.Button, accent) end if imgui.Button(khText(companion.name or 'Мой спутник') .. '##kh_custom_window_' .. tostring(companion.id), imgui.ImVec2(-1, 28)) then khCustomSelect(companion.id) end if selected then imgui.PopStyleColor() end end end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_custom_window_editor', imgui.ImVec2(0, 0), true) local selectedCustom = khCustomFind(khCustomSelectedId) if selectedCustom == nil then imgui.TextColored(muted, khText('Выбери спутника слева или создай нового.')) else imgui.TextColored(accent, khText('Настройка: ' .. tostring(selectedCustom.name or 'Мой спутник'))) imgui.Separator() imgui.Text(khText('Имя')) local customNameChanged = khCustomNameInput ~= nil and imgui.InputText('##kh_custom_window_name', khCustomNameInput, 96) or false imgui.Text(khText('Фраза появления')) local customHelloChanged = khCustomHelloInput ~= nil and imgui.InputText('##kh_custom_window_hello', khCustomHelloInput, 256) or false imgui.Text(khText('Фраза при перемещении')) local customMoveChanged = khCustomMoveInput ~= nil and imgui.InputText('##kh_custom_window_move', khCustomMoveInput, 256) or false imgui.Text(khText('Фраза после закрепления позиции')) local customSavedChanged = khCustomSavedInput ~= nil and imgui.InputText('##kh_custom_window_saved', khCustomSavedInput, 256) or false if imgui.Button(khText('Выбрать картинку') .. '##kh_custom_window_image', imgui.ImVec2(190, 28)) then if khCustomChooseImage() == nil then nakhodkaNotify('Картинка не выбрана или не удалось скопировать файл.', -1, 'warning', 3) else nakhodkaNotify('Картинка спутника сохранена.', -1, 'success', 2) end end imgui.SameLine() imgui.TextWrapped(khText(selectedCustom.image ~= '' and selectedCustom.image or 'Картинка не выбрана')) imgui.Text(khText('Обычные фразы, по одной на строку')) local normalChanged = false if imgui.InputTextMultiline ~= nil then normalChanged = khCustomPhrasesInput ~= nil and imgui.InputTextMultiline('##kh_custom_window_phrases', khCustomPhrasesInput, 16384, imgui.ImVec2(-1, 126)) or false else normalChanged = khCustomPhrasesInput ~= nil and imgui.InputText('##kh_custom_window_phrases', khCustomPhrasesInput, 16384) or false end imgui.Text(khText('Фразы после события, по одной на строку')) local celebrateChanged = false if imgui.InputTextMultiline ~= nil then celebrateChanged = khCustomCelebrateInput ~= nil and imgui.InputTextMultiline('##kh_custom_window_celebrate', khCustomCelebrateInput, 16384, imgui.ImVec2(-1, 126)) or false else celebrateChanged = khCustomCelebrateInput ~= nil and imgui.InputText('##kh_custom_window_celebrate', khCustomCelebrateInput, 16384) or false end if customNameChanged or customHelloChanged or customMoveChanged or customSavedChanged or normalChanged or celebrateChanged then khCustomCommitEditor() end imgui.Separator() if imgui.Button(khText('Удалить спутника') .. '##kh_custom_window_delete', imgui.ImVec2(-1, 30)) then local deletedName = selectedCustom.name or 'спутник' if khCustomDelete(selectedCustom.id) then khCustomRestoreBuiltInGlobals() nakhodkaNotify('Спутник удален: ' .. tostring(deletedName), -1, 'info', 3) end end end imgui.EndChild() end imgui.End() 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) local compactBuild = not khFeatureEnabled('markers3d') and not khFeatureEnabled('pointcheck') and not khFeatureEnabled('team') and not khFeatureEnabled('companion') and not khFeatureEnabled('dopki') and not khFeatureEnabled('digCursor') local mainPanelHeight = compactBuild and math.min(mainHeight, 325) or mainHeight if mainHeight + hudHeight + gap > avail.y then -- Оставляем главному блоку немного больше места: кнопка обновления -- меток и время последней загрузки не должны попадать под нижнюю панель. mainHeight = math.max(390, 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, mainPanelHeight), true, noScrollFlags) sectionTitle('Вспомогательная панель') imgui.PushStyleVarVec2(imgui.StyleVar.ItemSpacing, imgui.ImVec2(12, 1)) imgui.Columns(2, '##kh_main_toggles', false) imgui.SetColumnWidth(0, math.max(300, avail.x * 0.42)) if khToggle(khText('Включить скрипт'), khScriptEnabled) then khSaveMainSettings() khApplyScriptEnabledState() end khHelp('Полностью включает или выключает функционал Nakhodka: зоны, метки, допки, статистику и 3D маркеры.') imgui.NextColumn() if khToggle(khText('Только в пределах зоны'), khOnlyInZone) then khSaveMainSettings() khRefreshMainTreasureBlips(true) end khHelp('Основные метки показываются только внутри активной зоны клада. Когда зона не активна, метки скрываются.') imgui.NextColumn() if khToggle(khText('\xce\xef\xe5\xf0\xe0\xf6\xe8\xe8 \xf1 \xe7\xee\xed\xee\xe9'), khZoneOpsEnabled) then khSaveMainSettings() end khHelp('Включает или выключает работу скрипта с зоной: перехват, восстановление и команды зон (/zr, /zi, /zc, /zp, /zd).') imgui.NextColumn() if khZoneOpsEnabled[0] then if khToggle(khText('\xc2\xfb\xea\xeb\xfe\xf7\xe8\xf2\xfc \xec\xe8\xe3\xe0\xfe\xf9\xf3\xfe \xe7\xee\xed\xf3'), khDisableZoneBlink) then khSaveMainSettings() end khHelp('\xcf\xee \xf3\xec\xee\xeb\xf7\xe0\xed\xe8\xfe \xe7\xee\xed\xe0 \xec\xe8\xe3\xe0\xe5\xf2. \xc2\xea\xeb\xfe\xf7\xe8\xf2\xe5 \xf2\xf3\xec\xe1\xeb\xe5\xf0, \xf7\xf2\xee\xe1\xfb \xee\xf1\xf2\xe0\xe2\xe8\xf2\xfc \xe7\xee\xed\xf3 \xf1\xf2\xe0\xf2\xe8\xf7\xed\xee\xe9.') end imgui.NextColumn() if khFeatureEnabled('companion') then 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('Показывает выбранного спутника с облачком фраз. В обычном режиме он закреплен.') end imgui.NextColumn() if khFeatureEnabled('digCursor') then if khToggle(khText('Фиксированный курсор при копке'), khFixedDigCursorEnabled) then if not khFixedDigCursorEnabled[0] then khStopDigCursorLock() end khSaveMainSettings() end khHelp('Во время мини-игры копания сразу ставит курсор на центральную иконку и удерживает его там до завершения. Остается только нажимать; после закрытия мини-игры курсор освобождается.') end imgui.NextColumn() if khToggle(khText('Скрыть иконки'), khHideServerIcons) then khSaveMainSettings() end khHelp('Скрывает мешающие иконки бизнесов и другие серверные значки на карте. После активации зайдите и выйдите из интерьера.') imgui.NextColumn() if khToggle(khText('Уведомления'), khNotificationsEnabled) then khSaveMainSettings() end imgui.SameLine() if imgui.Button(khText('Настройка уведомлений') .. '##kh_notify_settings', imgui.ImVec2(190, 26)) then khNotificationSettingsOpen[0] = true end khHelp('Главный переключатель уведомлений Nakhodka. Отдельные категории настраиваются соседней кнопкой.') imgui.Columns(1) imgui.PopStyleVar() if khFeatureEnabled('companion') then 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 local customCompanionSelected = khCustomFind(khCustomActiveId) ~= nil if not customCompanionSelected and 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 not customCompanionSelected and shownVariant == 0 then imgui.PopStyleColor() end imgui.SameLine() if not customCompanionSelected and 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 not customCompanionSelected and shownVariant == 1 then imgui.PopStyleColor() end imgui.SetCursorPosY(versionRowY + 34) local companionActionsWidth = imgui.GetContentRegionAvail().x local companionActionW = math.max(112, math.floor((companionActionsWidth - 16) / 3)) if imgui.Button(khText('Изменить положение') .. '##kh_companion_move', imgui.ImVec2(companionActionW, 30)) then khTyanEnabled[0] = true khTyanMoveMode = true khTyanDragging = false khTyanMoveSaveLatch = true khTyanSay(khTyanMoveStartPhrase(), true, 2.8) khSaveMainSettings() end imgui.SameLine(0, 8) if imgui.Button(khText('Создать своего') .. '##kh_custom_companion_create', imgui.ImVec2(companionActionW, 30)) then khCustomCreate() khCustomWindowOpen[0] = true end imgui.SameLine(0, 8) if imgui.Button(khText('Мои спутники') .. '##kh_custom_companion_open', imgui.ImVec2(companionActionW, 30)) then khCustomWindowOpen[0] = true end imgui.TextColored(khThemeColor(nil, 'muted'), khText('Перемещение доступно после нажатия кнопки. Свои спутники редактируются отдельно.')) 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, 200, 800, '') then local nextSize = math.floor((tonumber(khTyanSizeInput[0]) or khTyanSize or 260) + 0.5) if nextSize < 200 then nextSize = 200 end if nextSize > 800 then nextSize = 800 end khTyanSize = nextSize khTyanSizeInput[0] = khTyanSize khSaveMainSettings() end imgui.PopItemWidth() imgui.SetCursorPosY(sizeRowY + 29) imgui.TextColored(khThemeColor(nil, 'muted'), khText('Размер применяется к выбранному встроенному или пользовательскому спутнику.')) end 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 khFeatureEnabled('pointcheck') then sliderRow('Радиус проверки точек', '##kh_point_check_radius', khPointCheckRadius, 0, 100, 'Если проехать рядом с точкой в этом радиусе, она считается проверенной. Значение 0 полностью отключает автоматическую проверку точек.') end if sliderRow('Иконка точки на карте', '##kh_point_icon', khPointIcon, 1, 63, 'Иконка основных кладов на миникарте. По умолчанию 56.') then khRefreshMainTreasureBlips(true) end imgui.Dummy(imgui.ImVec2(0, 3)) if imgui.Button(khText(khMarksUpdating and 'Обновляю точки...' or 'Перезагрузить точки'), imgui.ImVec2(-1, 30)) then khStartMainMarksRemoteUpdate() end local marksUpdateText = khMarksLastUpdateText ~= nil and tostring(khMarksLastUpdateText) or '' if marksUpdateText == '' then marksUpdateText = 'еще не обновлялись' end imgui.SetCursorPosY(imgui.GetCursorPosY() - 3) imgui.TextColored(khThemeColor(nil, 'muted'), khText('Последнее обновление меток: ' .. marksUpdateText)) 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 khRenderNotificationSettingsWindow() if khNotificationSettingsOpen == nil or not khNotificationSettingsOpen[0] then return end if imgui.SetNextWindowSize ~= nil then imgui.SetNextWindowSize(imgui.ImVec2(430, 430), imgui.Cond.FirstUseEver) end if not imgui.Begin(khText('Настройка уведомлений') .. '##kh_notification_settings_window', khNotificationSettingsOpen) then imgui.End() return end imgui.TextWrapped(khText('Выберите события, по которым должны приходить уведомления. Настройки сохраняются автоматически.')) imgui.Separator() local rows = { {'Активация карты', 'map', 'Активация карты кладов и сообщения о найденной территории.'}, {'Зоны и таймеры', 'zones', 'Изменения зон, восстановление и окончание кулдауна.'}, {'Точки и допки', 'points', 'Метки, основные точки, допки и чекпоинты.'}, {'Дроп и статистика', 'drops', 'Дроп предметов, цены, прибыль и статистика.'}, {'Команда и SOS', 'team', 'Командные события, приглашения, союзники, GPS и SOS.'}, {'Обновления', 'updates', 'Проверка версии, загрузка точек и обновление файлов.'}, {'Спутник', 'companion', 'События и настройки выбранного спутника.'}, {'Системные сообщения', 'system', 'Остальные сообщения скрипта и ошибки.'} } for _, row in ipairs(rows) do local toggle = khNotifyCategories[row[2]] if khToggle(khText(row[1]), toggle) then khSaveMainSettings() end khHelp(row[3]) end imgui.End() end function khRender3DMarkersWindow() local changed = false 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 accent = khThemeColor(nil, 'accentText') local muted = khThemeColor(nil, 'muted') local function saveIfChanged(value) if value then changed = true end end local function sliderLine(label, id, value, minValue, maxValue, helpText) local rowY = imgui.GetCursorPosY() imgui.SetCursorPosY(rowY + 2) imgui.Text(khText(label)) khHelp(helpText) imgui.SameLine() imgui.SetCursorPosY(rowY) imgui.SetCursorPosX(260) imgui.PushItemWidth(-10) saveIfChanged(imgui.SliderInt(id, value, minValue, maxValue)) imgui.PopItemWidth() imgui.SetCursorPosY(rowY + 31) end local function sliderMeterLine(label, id, value, minValue, maxValue, helpText) local rowY = imgui.GetCursorPosY() imgui.SetCursorPosY(rowY + 2) imgui.Text(khText(label)) khHelp(helpText) imgui.SameLine() imgui.SetCursorPosY(rowY) imgui.SetCursorPosX(260) imgui.PushItemWidth(-10) local changedNow = false local currentValue = tonumber(value[0]) or minValue if imgui.SliderFloat ~= nil then local temp = ffi.new('float[1]', currentValue) local ok = false ok, changedNow = pcall(function() return imgui.SliderFloat(id, temp, minValue, maxValue, '%.1f') end) if not ok then ok, changedNow = pcall(function() return imgui.SliderFloat(id, temp, minValue, maxValue) end) end if ok and changedNow then value[0] = khClampFloat(temp[0], minValue, minValue, maxValue) end else local temp = new.int(math.floor(currentValue * 10 + 0.5)) changedNow = imgui.SliderInt(id, temp, math.floor(minValue * 10 + 0.5), math.floor(maxValue * 10 + 0.5)) if changedNow then value[0] = khClampFloat((tonumber(temp[0]) or 0) / 10, minValue, minValue, maxValue) end end imgui.PopItemWidth() saveIfChanged(changedNow) imgui.SetCursorPosY(rowY + 31) end imgui.PushStyleVarVec2(imgui.StyleVar.ItemSpacing, imgui.ImVec2(10, 7)) imgui.BeginChild('##kh_3d_marker_settings', imgui.ImVec2(0, 0), true, 0) imgui.TextColored(accent, khText('Режим отображения')) imgui.Separator() imgui.Columns(2, '##kh_3d_toggles', false) imgui.SetColumnWidth(0, math.max(360, imgui.GetContentRegionAvail().x * 0.50)) saveIfChanged(khToggle(khText('Включить 3D маркеры'), kh3DMarkersEnabled)) khHelp('Включает отображение основных точек и допок прямо в мире.') imgui.NextColumn() saveIfChanged(khToggle(khText('Проверка препятствий (Anti-WH)'), kh3DMarkersAntiWh)) khHelp('Маркеры будут видны только если между вами и точкой нет стен/заборов. Работает и для кругов, и для обычных pickup-маркеров.') imgui.Columns(1) imgui.Dummy(imgui.ImVec2(0, 2)) imgui.Text(khText('Тип маркеров:')) khHelp('Красивые маркеры рисуют круги на земле, обычные ставят белые стрелочки в фиксированном радиусе 30 метров.') local modeButtonWidth = math.max(1, math.floor((imgui.GetContentRegionAvail().x - 10) / 2)) local shownMarkerType = kh3DPendingMarkerType ~= nil and kh3DPendingMarkerType or kh3DMarkerType[0] local theme3d = khThemeCurrent() imgui.PushStyleColor(imgui.Col.Button, shownMarkerType == 0 and khThemeColor(theme3d, 'buttonActive') or khThemeColor(theme3d, 'tabIdle')) imgui.PushStyleColor(imgui.Col.ButtonHovered, khThemeColor(theme3d, 'buttonHovered')) imgui.PushStyleColor(imgui.Col.ButtonActive, khThemeColor(theme3d, 'buttonActive')) if imgui.Button(khText('Красивые (круги)'), imgui.ImVec2(modeButtonWidth, 30)) then kh3DRequestMarkerType(0) end imgui.PopStyleColor(3) imgui.SameLine() imgui.PushStyleColor(imgui.Col.Button, shownMarkerType == 1 and khThemeColor(theme3d, 'buttonActive') or khThemeColor(theme3d, 'tabIdle')) imgui.PushStyleColor(imgui.Col.ButtonHovered, khThemeColor(theme3d, 'buttonHovered')) imgui.PushStyleColor(imgui.Col.ButtonActive, khThemeColor(theme3d, 'buttonActive')) if imgui.Button(khText('Обычные'), imgui.ImVec2(modeButtonWidth, 30)) then kh3DRequestMarkerType(1) end imgui.PopStyleColor(3) imgui.Dummy(imgui.ImVec2(0, 3)) imgui.Separator() if shownMarkerType == 0 then imgui.TextColored(accent, khText('Цвета основных кругов')) sliderLine('Дистанция показа кругов', '##kh_3d_distance', kh3DMarkerDistance, 30, 300, 'Максимальная дистанция, на которой круги будут отображаться.') sliderMeterLine('Радиус красивых кругов, м', '##kh_3d_radius', kh3DMarkerRadius10, 0.5, 5.0, 'Сколько метров занимает круг. Можно ставить дробные значения.') imgui.Dummy(imgui.ImVec2(0, 4)) imgui.Separator() imgui.TextColored(accent, khText('Цвета основных кругов')) imgui.TextWrapped(khText('Палитра меняет только основные круги. Допки всегда остаются радужными.')) local colorButtonWidth = math.max(1, math.floor((imgui.GetContentRegionAvail().x - 10) / 2)) imgui.PushStyleColor(imgui.Col.Button, kh3DColorTarget == 0 and khThemeColor(theme3d, 'buttonActive') or khThemeColor(theme3d, 'tabIdle')) imgui.PushStyleColor(imgui.Col.ButtonHovered, khThemeColor(theme3d, 'buttonHovered')) imgui.PushStyleColor(imgui.Col.ButtonActive, khThemeColor(theme3d, 'buttonActive')) if imgui.Button(khText('Непроверенные'), imgui.ImVec2(colorButtonWidth, 28)) then kh3DColorTarget = 0 end imgui.PopStyleColor(3) imgui.SameLine() imgui.PushStyleColor(imgui.Col.Button, kh3DColorTarget == 1 and khThemeColor(theme3d, 'buttonActive') or khThemeColor(theme3d, 'tabIdle')) imgui.PushStyleColor(imgui.Col.ButtonHovered, khThemeColor(theme3d, 'buttonHovered')) imgui.PushStyleColor(imgui.Col.ButtonActive, khThemeColor(theme3d, 'buttonActive')) if imgui.Button(khText('Проверенные'), imgui.ImVec2(colorButtonWidth, 28)) then kh3DColorTarget = 1 end imgui.PopStyleColor(3) local rVar, gVar, bVar = kh3DNormalR, kh3DNormalG, kh3DNormalB if kh3DColorTarget == 1 then rVar, gVar, bVar = kh3DCheckedR, kh3DCheckedG, kh3DCheckedB end local function set3DColor(r, g, b) rVar[0] = math.max(0, math.min(255, math.floor((tonumber(r) or 0) + 0.5))) gVar[0] = math.max(0, math.min(255, math.floor((tonumber(g) or 0) + 0.5))) bVar[0] = math.max(0, math.min(255, math.floor((tonumber(b) or 0) + 0.5))) changed = true end local colorAreaHeight = 192 local leftColorWidth = math.max(220, math.floor(imgui.GetContentRegionAvail().x * 0.36)) imgui.Columns(2, '##kh_3d_color_columns', false) imgui.SetColumnWidth(0, leftColorWidth) imgui.BeginChild('##kh_3d_color_picker_box', imgui.ImVec2(0, colorAreaHeight), true, noScrollFlags) imgui.Indent(8) if kh3DPickerBuf == nil then kh3DPickerBuf = ffi.new('float[4]', {1, 1, 1, 1}) end local needSync = kh3DColorTarget ~= kh3DPickerLastTarget or (tonumber(rVar[0]) or 0) ~= kh3DPickerLastR or (tonumber(gVar[0]) or 0) ~= kh3DPickerLastG or (tonumber(bVar[0]) or 0) ~= kh3DPickerLastB if needSync then kh3DPickerBuf[0] = (tonumber(rVar[0]) or 255) / 255 kh3DPickerBuf[1] = (tonumber(gVar[0]) or 255) / 255 kh3DPickerBuf[2] = (tonumber(bVar[0]) or 255) / 255 kh3DPickerBuf[3] = 1.0 kh3DPickerLastTarget = kh3DColorTarget end local color = kh3DPickerBuf 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 if imgui.ColorEditFlags.NoAlpha ~= nil then pickerFlags = pickerFlags + imgui.ColorEditFlags.NoAlpha end end if imgui.PushItemWidth ~= nil then imgui.PushItemWidth(170) end local colorChanged = false if imgui.ColorPicker4 ~= nil then imgui.SetCursorPosX(imgui.GetCursorPosX() + math.max(0, (imgui.GetContentRegionAvail().x - 170) * 0.5)) colorChanged = imgui.ColorPicker4('##kh_3d_main_color_picker_' .. tostring(kh3DColorTarget), color, pickerFlags) elseif imgui.ColorEdit4 ~= nil then colorChanged = imgui.ColorEdit4('##kh_3d_main_color_picker_' .. tostring(kh3DColorTarget), color) end if imgui.PopItemWidth ~= nil then imgui.PopItemWidth() end imgui.Unindent(8) if colorChanged then set3DColor(color[0] * 255, color[1] * 255, color[2] * 255) end kh3DPickerLastR = tonumber(rVar[0]) or 0 kh3DPickerLastG = tonumber(gVar[0]) or 0 kh3DPickerLastB = tonumber(bVar[0]) or 0 imgui.EndChild() imgui.NextColumn() imgui.BeginChild('##kh_3d_color_presets_box', imgui.ImVec2(0, colorAreaHeight), true, noScrollFlags) imgui.TextColored(accent, khText('Быстрые цвета')) imgui.Separator() local presets = { {khText('Белый'), 255, 255, 255}, {khText('Красный'), 255, 51, 51}, {khText('Зелёный'), 75, 210, 105}, {khText('Синий'), 80, 145, 255}, {khText('Жёлтый'), 255, 215, 70}, {khText('Розовый'), 255, 95, 180}, {khText('Бирюза'), 45, 225, 215}, {khText('Фиолетовый'), 185, 115, 245} } local presetCols = 2 local presetGap = 6 local presetAvail = imgui.GetContentRegionAvail() local presetW = math.max(1, math.floor((presetAvail.x - presetGap * (presetCols - 1)) / presetCols)) for i, preset in ipairs(presets) do if (i - 1) % presetCols ~= 0 then imgui.SameLine(0, presetGap) end local pr, pg, pb = preset[2], preset[3], preset[4] imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(pr / 255 * 0.42 + 0.04, pg / 255 * 0.42 + 0.04, pb / 255 * 0.42 + 0.04, 1.00)) imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(pr / 255 * 0.60 + 0.05, pg / 255 * 0.60 + 0.05, pb / 255 * 0.60 + 0.05, 1.00)) imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(pr / 255 * 0.76 + 0.06, pg / 255 * 0.76 + 0.06, pb / 255 * 0.76 + 0.06, 1.00)) if imgui.Button(preset[1] .. '##kh_3d_color_preset_' .. tostring(kh3DColorTarget) .. '_' .. tostring(i), imgui.ImVec2(presetW, 22)) then set3DColor(pr, pg, pb) end imgui.PopStyleColor(3) if i % presetCols == 0 then imgui.Dummy(imgui.ImVec2(0, 2)) end end imgui.EndChild() imgui.Columns(1) end imgui.Dummy(imgui.ImVec2(0, 3)) imgui.Separator() imgui.TextColored(accent, khText('Информация')) imgui.TextWrapped(khText('Красивые 3D маркеры - это круги на земле: основные точки берут цвета из палитры, а допки всегда остаются радужными. Обычные 3D маркеры - это белые стрелочки над ближайшими объектами в фиксированном радиусе 30 метров.')) if changed then khSaveMainSettings() end imgui.EndChild() imgui.PopStyleVar() end 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 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 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 imgui.OpenPopup(khText('Очистка лога') .. '##kh_drop_clear_confirm') end local confirmFlags = 0 if imgui.WindowFlags.AlwaysAutoResize ~= nil then confirmFlags = confirmFlags + imgui.WindowFlags.AlwaysAutoResize end if imgui.WindowFlags.NoSavedSettings ~= nil then confirmFlags = confirmFlags + imgui.WindowFlags.NoSavedSettings end if imgui.BeginPopupModal(khText('Очистка лога') .. '##kh_drop_clear_confirm', nil, confirmFlags) then imgui.Text(khText('Действительно очистить весь лог статистики?')) imgui.TextColored(khThemeColor(nil, 'muted'), khText('Удалятся все записи дропа и введённые цены. Действие необратимо.')) imgui.Separator() if imgui.Button(khText('Да, очистить') .. '##kh_drop_clear_yes', imgui.ImVec2(150, 28)) then khDropLogs = {} khDropPrices = {} khPriceInputs = {} khInvalidateAveragePriceCache() khDropSelectedDate = nil khDropSelectedLogId = nil khCurrentDropLogId = nil khPendingDropSession = nil khInvalidateDropProfitCache() khSaveDropStats() khSaveDropPrices() nakhodkaNotify('История дропа и цены очищены.', -1, 'success', 3) imgui.CloseCurrentPopup() end imgui.SameLine() if imgui.Button(khText('Отмена') .. '##kh_drop_clear_no', imgui.ImVec2(110, 28)) then imgui.CloseCurrentPopup() end imgui.EndPopup() 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 function khRenderDopkiWindow() if not khFeatureEnabled('dopki') then return end 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('Показывать метку допки'), khDopShowBlips) then khRefreshAdditionalBlips() end imgui.Text(khText('Иконка допок')) 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 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, 0) 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), 12, 15, 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, 0) imgui.TextColored(khThemeColor(theme, 'accentText'), khText('Файлы')) imgui.Separator() if khBlur ~= nil then if khToggle(khText('Размытие фона'), khMenuBlurEnabled) then khSaveMainSettings() end if khMenuBlurEnabled ~= nil and khMenuBlurEnabled[0] then 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 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 на сайте и ставит свежий Lua, если мы залили фикс.') if imgui.Button(khText(khUpdateChecking and 'Проверяю...' or 'Проверить обновления'), imgui.ImVec2(-1, 28)) then khCheckForScriptUpdate(true) end imgui.TextColored(khThemeColor(theme, 'muted'), khText('Текущая версия: ') .. tostring(NK_SCRIPT_VERSION_TEXT or NK_SCRIPT_VERSION or '—')) 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) dl = (ok2 and tonumber(dl)) or -1 khFilesChecking = false if dl < 0 then nakhodkaNotify('Не удалось проверить файлы: манифест или загрузка недоступны.', -1, 'error', 4) elseif 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, 'success', 3) end end) end end imgui.Dummy(imgui.ImVec2(0, 6)) if not khResetSettingsConfirm then if imgui.Button(khText('Сбросить настройки'), imgui.ImVec2(-1, 28)) then khResetSettingsConfirm = true end else imgui.TextWrapped(khText('Вы действительно хотите сбросить настройки?')) if imgui.Button(khText('Подтвердить сброс'), imgui.ImVec2(-1, 28)) then khResetSettingsConfirm = false 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 khUnloadKey = 0 khUnloadKeyWaiting = false pcall(khResetHudPosition) khSaveMainSettings() nakhodkaNotify('Настройки сброшены', -1, 'success', 3) end if imgui.Button(khText('Отмена') .. '##kh_reset_settings_cancel', imgui.ImVec2(-1, 28)) then khResetSettingsConfirm = false end end imgui.Dummy(imgui.ImVec2(0, 6)) imgui.Separator() imgui.TextColored(khThemeColor(theme, 'accentText'), khText('Быстрая выгрузка')) if imgui.Button(khText('Выгрузить Nakhodka'), imgui.ImVec2(-1, 28)) then khUnloadNakhodka('menu') end local unloadKeyCode = math.floor((tonumber(khUnloadKey) or 0) + 0.5) local unloadBindText = khUnloadKeyWaiting and 'Нажми любую клавишу' or (unloadKeyCode > 0 and ('Бинд: ' .. khTeamMapKeyName(unloadKeyCode)) or 'Назначить бинд выгрузки') if imgui.Button(khText(unloadBindText) .. '##kh_unload_key', imgui.ImVec2(-1, 26)) then khTeamMapKeyWaiting = false khUnloadKeyWaiting = true nakhodkaNotify('Нажми клавишу или Mouse 4/5 для выгрузки. Esc - отмена.', -1, 'info', 3) end if unloadKeyCode > 0 and not khUnloadKeyWaiting then if imgui.Button(khText('Убрать бинд') .. '##kh_unload_key_clear', imgui.ImVec2(-1, 24)) then khUnloadKey = 0 khSaveMainSettings() nakhodkaNotify('Бинд выгрузки удалён.', -1, 'success', 2) end end khHelp('Команды: /khunload и /khunl. Выгрузка отключает Lua до перезапуска или ручной загрузки скрипта.') imgui.Dummy(imgui.ImVec2(0, 6)) imgui.Separator() imgui.TextColored(imgui.ImVec4(1.00, 0.34, 0.40, 1.00), khText('Удаление')) if not khUninstallConfirm then if imgui.Button(khText('Удалить Nakhodka с компьютера'), imgui.ImVec2(-1, 28)) then khUninstallConfirm = true end else imgui.TextWrapped(khText('Вы действительно хотите удалить Nakhodka?')) if imgui.Button(khText('Подтвердить удаление'), imgui.ImVec2(-1, 28)) then khUninstallConfirm = false khConfirmUninstallNakhodka() end if imgui.Button(khText('Отмена'), imgui.ImVec2(-1, 28)) then khUninstallConfirm = false end end imgui.EndChild() imgui.Columns(1) imgui.EndChild() end 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 - это твой помощник для поиска кладов: хранит основные точки и допки, показывает метки на карте и в 3D, помогает отмечать проверенные места, вести цены, считать прибыль и смотреть статистику выкопанных кладов. Все нужное собрано в одном аккуратном окне без лишней беготни по командам.')) if imgui.Button(khText('Telegram канал Nakhodka'), imgui.ImVec2(230, 30)) then khOpenExternalUrl('https://t.me/NakhodkaLua') end imgui.SameLine() imgui.TextColored(muted, khText('t.me/NakhodkaLua')) 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('Метки и 3D')) imgui.TextWrapped(khText('Основные точки, допки, GPS/чекпоинты, красивые круги на земле и обычные белые стрелки на 30 метров.')) 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, 720), 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.IsItemClicked ~= nil then local okClick, clickedTitle = pcall(imgui.IsItemClicked) if okClick and clickedTitle then khHandleTitleReloadClick() end end 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 local tabInfo = khTabs[i] if khIsTabAvailable(tabInfo) then khRenderTabButton(i) end end imgui.EndChild() imgui.SameLine() imgui.BeginChild('##kh_content', imgui.ImVec2(0, 0), true) local tab = khTabs[khActiveTab] if not khIsTabAvailable(tab) then khActiveTab = 1 khTargetTab = 1 tab = khTabs[khActiveTab] end imgui.PushStyleVarFloat(imgui.StyleVar.Alpha, khContentAlpha) imgui.TextColored(khThemeColor(nil, 'accentText'), khText(tab.title)) imgui.TextWrapped(khText(tab.desc)) imgui.Separator() if khActiveTab == 1 then khRenderMainSettingsWindow() elseif khActiveTab == 2 then khRender3DMarkersWindow() elseif khActiveTab == 3 then khRenderTeamWindow() elseif khActiveTab == 4 then khRenderMainPointsWindow() elseif khActiveTab == 5 then khRenderDropStatsWindow() elseif khActiveTab == 6 then khRenderPricesWindow() elseif khActiveTab == 7 then khRenderDopkiWindow() elseif khActiveTab == 8 then khRenderSettingsWindow() elseif khActiveTab == 9 then khRenderInfoWindow() else khRenderEmptySection() end imgui.PopStyleVar() imgui.EndChild() end imgui.End() khRenderCustomCompanionsWindow() khRenderSpawnsWindow() khRenderNotificationSettingsWindow() 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 = {} -- Server gang-zone snapshots used by /dgz and /rgz. Coordinates and colors -- must be preserved before the local RPC removes a zone from the radar. local khGangZoneSnapshots = {} local khDeletedGangZoneSnapshots = {} local khGangZonesHidden = false local khGangZoneLocalAdding = {} local khGangZoneLocalRemoving = {} function khGetGangZoneMapState() return khGangZoneSnapshots,khGangZonesHidden end khZoneNeedsRestore = false khZoneRestoreAt = 0 -- Спавны: локальные данные для быстрого реконнекта к ближайшему месту от зоны. khSpawnFeatureEnabled = imguiReady and new.bool(true) or {[0] = true} khSpawnMapEnabled = imguiReady and new.bool(false) or {[0] = false} khSpawnWindowOpen = imguiReady and new.bool(false) or {[0] = false} khSpawnRecords = {} khSpawnDialogEntries = {} khSpawnDialogId = nil khSpawnDialogOpen = false khSpawnDialogShownAt = 0 khSpawnPendingTarget = nil khSpawnPendingTargetUntil = 0 khSpawnPendingArrival = nil khSpawnArrivalAt = 0 khSpawnTrailerWait = nil khSpawnTrailerSawInterior = false khSpawnNearest = nil khSpawnNearestBlip = nil khSpawnNearestKey = '' khSpawnNearestRefreshAt = 0 khSpawnMapClickAt = 0 khSpawnScan = nil khSpawnLastCheckpoint = nil khSpawnLastCheckpointAt = 0 khSpawnCheckpointSerial = 0 khSpawnCheckpointOwner = nil function khSpawnLower(text) text = tostring(text or '') text = text:gsub('[\192-\223]', function(ch) return string.char(ch:byte() + 32) end) return text:lower() end function khSpawnTrim(text) text = tostring(text or '') -- CEF иногда ставит цвет до номера строки: сначала снимаем все цвет-коды, -- затем служебный индекс /rec. ID дома внутри названия остаётся нетронутым. text = text:gsub('{%x%x%x%x%x%x}', '') text = text:gsub('^%s*%[%d+%]%s*', '') -- The game font renders CP1251 numero as a question mark. text = text:gsub(string.char(185) .. '%s*(%d+)', '#%1') text = text:gsub('%?%s*(%d+)', '#%1') text = text:gsub('^%s+', ''):gsub('%s+$', '') return text end function khSpawnCleanField(text) return tostring(text or ''):gsub('|', '/') end function khSpawnIsLastExit(rawName) local lower = khSpawnLower(rawName) return lower:find('последнее место выхода', 1, true) ~= nil or lower:find('последний выход', 1, true) ~= nil end function khSpawnDetectType(rawName) local lower = khSpawnLower(rawName) if lower:find('\xf1\xe5\xec\xe5\xe9\xed', 1, true) ~= nil then return 'family', nil end local houseId = lower:match('#%s*(%d+)') or lower:match('\xe4\xee\xec%s*(%d+)') or lower:match('\xe4\xee\xec[^%d]*(%d+)') if lower:find('\xe4\xee\xec', 1, true) ~= nil and houseId ~= nil then return 'house', tonumber(houseId) end if lower:find('\xf2\xf0\xe5\xe9\xeb\xe5\xf0', 1, true) ~= nil then return 'trailer', nil end if lower:find('\xee\xf2\xe5\xeb\xfc', 1, true) ~= nil then return 'hotel', nil end if lower:find('\xee\xf0\xe3\xe0\xed\xe8\xe7\xe0\xf6', 1, true) ~= nil or lower:find('\xf4\xf0\xe0\xea\xf6', 1, true) ~= nil then return 'organization', nil end if lower:find('\xe2\xee\xea\xe7\xe0\xeb', 1, true) ~= nil then return 'station', nil end return 'custom', nil end function khSpawnIsTrackedType(spawnType) return spawnType == 'house' or spawnType == 'trailer' or spawnType == 'station' or spawnType == 'custom' end function khSpawnMakeKey(rawName, spawnType, houseId) if spawnType == 'house' and tonumber(houseId) ~= nil then return 'house:' .. tostring(math.floor(tonumber(houseId))) end return tostring(spawnType or 'custom') .. ':' .. khSpawnCleanField(khSpawnLower(rawName)) end function khSpawnHasCoords(entry) return type(entry) == 'table' and tonumber(entry.x) ~= nil and tonumber(entry.y) ~= nil and tonumber(entry.z) ~= nil end function khSpawnCurrentServerKey() local key = '' if type(khDetectActualServerKeyForTreasure) == 'function' then local ok, value = pcall(khDetectActualServerKeyForTreasure) if ok then key = tostring(value or '') end end if key == '' then key = 'unknown' end return key end function khSpawnRecordMatchesServer(entry, serverKey) if type(entry) ~= 'table' then return false end local own = tostring(entry.serverKey or '') if own == '' then return true end return own == tostring(serverKey or khSpawnCurrentServerKey()) end function khSpawnRecordsForCurrentServer() local serverKey = khSpawnCurrentServerKey() local result = {} for _, entry in ipairs(khSpawnRecords or {}) do if khSpawnRecordMatchesServer(entry, serverKey) then table.insert(result, entry) end end return result end function khSpawnFindRecordByKey(key) local serverKey = khSpawnCurrentServerKey() for _, entry in ipairs(khSpawnRecords or {}) do if tostring(entry.key or '') == tostring(key or '') and khSpawnRecordMatchesServer(entry, serverKey) then return entry end end return nil end function khSpawnFindRecord(rawName, spawnType, houseId) local key = khSpawnMakeKey(rawName, spawnType, houseId) return khSpawnFindRecordByKey(key) end function khSpawnEncodeRecord(entry) local hasCoords = khSpawnHasCoords(entry) and 1 or 0 return table.concat({ khSpawnCleanField(entry.key), khSpawnCleanField(entry.rawName), khSpawnCleanField(entry.type), tonumber(entry.houseId) and tostring(math.floor(tonumber(entry.houseId))) or '', tostring(hasCoords), string.format('%.4f', tonumber(entry.x) or 0), string.format('%.4f', tonumber(entry.y) or 0), string.format('%.4f', tonumber(entry.z) or 0), tostring(math.floor(tonumber(entry.updatedAt) or 0)), entry.missing and '1' or '0', tostring(math.floor(tonumber(entry.order) or 0)), tostring(math.floor(tonumber(entry.missCount) or 0)), khSpawnCleanField(entry.serverKey or '') }, '|') end function khSpawnDecodeRecord(line) local parts = {} for piece in (tostring(line or '') .. '|'):gmatch('([^|]*)|') do table.insert(parts, piece) end if #parts < 12 then return nil end local key, rawName = parts[1], parts[2] if key == nil or key == '' or rawName == '' then return nil end local entry = { key = key, rawName = rawName, type = parts[3] ~= '' and parts[3] or 'custom', houseId = tonumber(parts[4]), updatedAt = tonumber(parts[9]) or 0, missing = tostring(parts[10] or '') == '1', order = tonumber(parts[11]) or 0, missCount = tonumber(parts[12]) or 0, serverKey = tostring(parts[13] or '') } if tostring(parts[5] or '') == '1' then entry.x, entry.y, entry.z = tonumber(parts[6]), tonumber(parts[7]), tonumber(parts[8]) end return entry end function khSaveSpawnRecords() if mainCfg == nil then return false end if mainCfg.Spawns == nil then mainCfg.Spawns = {} end local oldCount = tonumber(mainCfg.Spawns.count) or 0 for i = 1, oldCount do mainCfg.Spawns['entry' .. tostring(i)] = nil end mainCfg.Spawns.enabled = khSpawnFeatureEnabled ~= nil and khSpawnFeatureEnabled[0] and true or false mainCfg.Spawns.mapEnabled = khSpawnMapEnabled ~= nil and khSpawnMapEnabled[0] and true or false mainCfg.Spawns.count = #khSpawnRecords for i, entry in ipairs(khSpawnRecords) do mainCfg.Spawns['entry' .. tostring(i)] = khSpawnEncodeRecord(entry) end return inicfg.save(mainCfg, config_file) end function khLoadSpawnRecords() khSpawnRecords = {} if mainCfg == nil then return end if mainCfg.Spawns == nil then mainCfg.Spawns = {enabled = true, mapEnabled = false, count = 0} end local count = tonumber(mainCfg.Spawns.count) or 0 local byKey = {} local changed = false for i = 1, count do local entry = khSpawnDecodeRecord(mainCfg.Spawns['entry' .. tostring(i)]) if entry ~= nil then local originalKey = tostring(entry.key or '') local originalRawName = tostring(entry.rawName or '') entry.rawName = khSpawnTrim(entry.rawName) entry.type, entry.houseId = khSpawnDetectType(entry.rawName) if not khSpawnIsTrackedType(entry.type) then changed = true else entry.key = khSpawnMakeKey(entry.rawName, entry.type, entry.houseId) if originalKey ~= entry.key or originalRawName ~= entry.rawName then changed = true end local mapKey = tostring(entry.serverKey or '') .. '@' .. tostring(entry.key or '') local existing = byKey[mapKey] if existing == nil then byKey[mapKey] = entry table.insert(khSpawnRecords, entry) else changed = true local preferEntry = khSpawnHasCoords(entry) and (not khSpawnHasCoords(existing) or (tonumber(entry.updatedAt) or 0) > (tonumber(existing.updatedAt) or 0)) if preferEntry then existing.x, existing.y, existing.z = entry.x, entry.y, entry.z existing.updatedAt = entry.updatedAt existing.missing = entry.missing end existing.order = math.min(tonumber(existing.order) or i, tonumber(entry.order) or i) existing.missing = existing.missing and entry.missing existing.missCount = math.min(tonumber(existing.missCount) or 0, tonumber(entry.missCount) or 0) end end end end table.sort(khSpawnRecords, function(a, b) local ao, bo = tonumber(a.order) or 0, tonumber(b.order) or 0 if ao == bo then return tostring(a.rawName or '') < tostring(b.rawName or '') end return ao < bo end) if changed then khSaveSpawnRecords() end end function khSpawnSyncDialog(text) if khIsViceCitySleepMode() then return {} end local serverKey = khSpawnCurrentServerKey() local seen = {} local rows = {} local rowIndex = 0 for line in tostring(text or ''):gmatch('[^\r\n]+') do local rawName = khSpawnTrim(line) if rawName ~= '' then if not khSpawnIsLastExit(rawName) then local spawnType, houseId = khSpawnDetectType(rawName) if khSpawnIsTrackedType(spawnType) then local key = khSpawnMakeKey(rawName, spawnType, houseId) local entry = khSpawnFindRecordByKey(key) if entry == nil then entry = { key = key, rawName = rawName, type = spawnType, houseId = houseId, order = #khSpawnRecords + 1, updatedAt = 0, missing = false, missCount = 0, serverKey = serverKey } table.insert(khSpawnRecords, entry) else entry.rawName = rawName entry.type = spawnType entry.houseId = houseId entry.missing = false entry.missCount = 0 entry.serverKey = serverKey end seen[key] = true table.insert(rows, {record = entry, rowIndex = rowIndex, rawName = rawName}) end end rowIndex = rowIndex + 1 end end for _, entry in ipairs(khSpawnRecords) do if entry.type == 'house' and khSpawnRecordMatchesServer(entry, serverKey) and not seen[tostring(entry.key or '')] then entry.missCount = (tonumber(entry.missCount) or 0) + 1 if entry.missCount >= 3 then entry.missing = true end end end khSpawnDialogEntries = rows khSaveSpawnRecords() return rows end function khSpawnFindDialogRow(key) for _, row in ipairs(khSpawnDialogEntries or {}) do if row.record ~= nil and tostring(row.record.key or '') == tostring(key or '') then return row end end return nil end function khSpawnRememberSelected(entry) if entry == nil then return end khSpawnPendingArrival = entry khSpawnArrivalAt = os.clock() local scan = khSpawnScan if scan ~= nil and scan.waiting ~= nil and scan.waiting.method == 'reconnect' and scan.waiting.record == entry then scan.waiting.selectedAt = khSpawnArrivalAt end khSpawnTrailerWait = nil khSpawnTrailerSawInterior = false khSpawnDialogOpen = false end function khSpawnRequestReconnect(entry) if entry == nil then return false end if khIsViceCitySleepMode() then nakhodkaNotify(sName .. 'На Vice City спавны временно недоступны.', -1, 'info', 3) return false end local scan = khSpawnScan local scanReconnect = scan ~= nil and scan.waiting ~= nil and scan.waiting.method == 'reconnect' and scan.waiting.record == entry if not scanReconnect and os.clock() < (tonumber(khSpawnMapClickAt) or 0) then return false end if not scanReconnect then khSpawnMapClickAt = os.clock() + 3.0 end khSpawnPendingTarget = tostring(entry.key or '') khSpawnPendingTargetUntil = os.clock() + 12.0 khSpawnPendingArrival = nil khSpawnTrailerWait = nil khSpawnTrailerSawInterior = false if type(sampProcessChatInput) == 'function' then sampProcessChatInput('/rec') nakhodkaNotify(sName .. 'Реконнект на ' .. tostring(entry.rawName or 'спавн'), -1, 'info', 3) return true end khSpawnPendingTarget = nil nakhodkaNotify(sName .. 'Не удалось запустить реконнект.', -1, 'error', 3) return false end function khSpawnHandleDialog(id, title, text) if khIsViceCitySleepMode() then return false end title = tostring(title or '') if not title:find('{BFBBBA}Выбор места спавна', 1, true) and not khSpawnLower(title):find('выбор места спавна', 1, true) then return false end khSpawnDialogId = tonumber(id) khSpawnDialogOpen = true khSpawnDialogShownAt = os.clock() khSpawnSyncDialog(text) if khSpawnPendingTarget ~= nil and os.clock() <= (tonumber(khSpawnPendingTargetUntil) or 0) then local row = khSpawnFindDialogRow(khSpawnPendingTarget) if row ~= nil and type(sampSendDialogResponse) == 'function' then khSpawnRememberSelected(row.record) khSpawnPendingTarget = nil khSpawnPendingTargetUntil = 0 sampSendDialogResponse(id, 1, tonumber(row.rowIndex) or 0, '') return true end khSpawnPendingTarget = nil khSpawnPendingTargetUntil = 0 nakhodkaNotify(sName .. 'Выбранный спавн не найден в списке.', -1, 'warning', 3) end return false end function khSpawnHandleDialogResponse(id, button, listboxId) if khSpawnDialogId == nil or tonumber(id) ~= tonumber(khSpawnDialogId) then return end khSpawnDialogOpen = false if tonumber(button) ~= 1 then return end local wantedIndex = tonumber(listboxId) for _, row in ipairs(khSpawnDialogEntries or {}) do if tonumber(row.rowIndex) == wantedIndex then khSpawnRememberSelected(row.record) return end end end function khSpawnSaveCoords(entry, x, y, z) if entry == nil then return false end x, y, z = tonumber(x), tonumber(y), tonumber(z) if x == nil or y == nil or z == nil then return false end entry.x, entry.y, entry.z = x, y, z entry.updatedAt = os.time() entry.missing = false entry.missCount = 0 khSaveSpawnRecords() khSpawnRefreshMapBlips(true) khSpawnRefreshNearestBlip(true) return true end function khSpawnSaveCurrentCoords(entry) if entry == nil or not sampIsLocalPlayerSpawned() then return false end local x, y, z = getCharCoordinates(PLAYER_PED) return khSpawnSaveCoords(entry, x, y, z) end function khSpawnTryFinishCheckpointScan(scan) if scan == nil or scan.waiting == nil then return false end local waiting = scan.waiting local candidate = waiting.candidate if waiting.confirmed and type(candidate) == 'table' then return khSpawnFinishScannedHouse(candidate.x, candidate.y, candidate.z) end return false end function khSpawnHandleScanServerMessage(message) local scan = khSpawnScan if scan == nil or scan.waiting == nil then return false end local waiting = scan.waiting if waiting.method ~= 'house' and waiting.method ~= 'trailer' then return false end local lower = khSpawnLower(message) local expected = nil if waiting.method == 'house' then expected = '\xED\xE0\x20\xEC\xE8\xED\xE8\xEA\xE0\xF0\xF2\xE5\x20\xEE\xF2\xEC\xE5\xF7\xE5\xED\xEE\x20\xEC\xE5\xF1\xF2\xEE\x2C\x20\xE3\xE4\xE5\x20\xF0\xE0\xF1\xEF\xEE\xEB\xEE\xE6\xE5\xED\x20\xE4\xEE\xEC' else expected = '\xEC\xE5\xF1\xF2\xEE\xEF\xEE\xEB\xEE\xE6\xE5\xED\xE8\xE5\x20\xE2\xE0\xF8\xE5\xE3\xEE\x20\xF2\xF0\xE5\xE9\xEB\xE5\xF0\xE0\x20\xEE\xF2\xEC\xE5\xF7\xE5\xED\xEE\x20\xE2\x20\xE2\xE0\xF8\xE5\xEC\x20\x67\x70\x73' end if lower:find(expected, 1, true) == nil then return false end waiting.confirmed = true waiting.confirmedAt = os.clock() khSpawnTryFinishCheckpointScan(scan) return true end function khSpawnBeginHouseScan(rescanAll) if khIsViceCitySleepMode() then nakhodkaNotify(sName .. '\xCD\xE0\x20\x56\x69\x63\x65\x20\x43\x69\x74\x79\x20\xF1\xEA\xE0\xED\x20\xF1\xEF\xE0\xE2\xED\xEE\xE2\x20\xED\xE5\xE4\xEE\xF1\xF2\xF3\xEF\xE5\xED\x2E', -1, 'info', 3) return false end if khSpawnDialogOpen or khSpawnPendingTarget ~= nil or khSpawnPendingArrival ~= nil or not sampIsLocalPlayerSpawned() then nakhodkaNotify(sName .. '\xC7\xE0\xEA\xF0\xEE\xE9\x20\xE4\xE8\xE0\xEB\xEE\xE3\x20\xE8\x20\xE4\xEE\xE6\xE4\xE8\xF1\xFC\x20\xF1\xEF\xE0\xE2\xED\xE0\x20\xEF\xE5\xF0\xE5\xE4\x20\xF1\xEA\xE0\xED\xEE\xEC\x20\xF1\xEF\xE0\xE2\xED\xEE\xE2\x2E', -1, 'info', 3) return false end local queue = khSpawnBuildScanQueue(rescanAll) if #queue == 0 then nakhodkaNotify(sName .. '\xCD\xE5\xF2\x20\xF1\xEF\xE0\xE2\xED\xEE\xE2\x20\xE4\xEB\xFF\x20\xF1\xEA\xE0\xED\xE0\x2E', -1, 'info', 2) return false end khSpawnScan = { active = true, queue = queue, index = 1, processed = 0, waiting = nil, lastSentAt = -100, nextAt = os.clock(), previousCheckpoint = khSpawnLastCheckpoint } nakhodkaNotify(sName .. '\xD1\xEA\xE0\xED\x20\xF1\xEF\xE0\xE2\xED\xEE\xE2\x20\xE7\xE0\xEF\xF3\xF9\xE5\xED\x2E', -1, 'info', 2) return true end function khSpawnStopHouseScan(silent) if khSpawnScan == nil then return end local previous = khSpawnScan.previousCheckpoint khSpawnScan = nil khSpawnCheckpointOwner = nil if type(sampSetCheckpoint) == 'function' and type(previous) == 'table' then pcall(sampSetCheckpoint, previous.x, previous.y, previous.z, previous.radius or 1.0) elseif type(disableCheckpoint) == 'function' then pcall(disableCheckpoint) end if not silent then nakhodkaNotify(sName .. '\xD1\xEA\xE0\xED\x20\xF1\xEF\xE0\xE2\xED\xEE\xE2\x20\xEE\xF1\xF2\xE0\xED\xEE\xE2\xEB\xE5\xED\x2E', -1, 'info', 2) end end function khSpawnBuildScanQueue(rescanAll) local queue = {} -- Очередь должна совпадать с порядком в окне спавнов: первый пункт -- списка действительно сканируется как 1/N, без скрытой перестановки. local methods = { house = 'house', trailer = 'trailer', station = 'reconnect', custom = 'reconnect' } for _, entry in ipairs(khSpawnRecordsForCurrentServer()) do local spawnType = tostring(entry.type or '') local method = methods[spawnType] if method ~= nil and (rescanAll or not khSpawnHasCoords(entry)) then if spawnType ~= 'house' or tonumber(entry.houseId) ~= nil then table.insert(queue, {record = entry, method = method}) end end end return queue end function khSpawnFinishScannedHouse(x, y, z) local scan = khSpawnScan if scan == nil or scan.waiting == nil then return false end local entry = scan.waiting.record if x ~= nil and y ~= nil and z ~= nil then khSpawnSaveCoords(entry, x, y, z) else entry.missing = true entry.missCount = (tonumber(entry.missCount) or 0) + 1 khSaveSpawnRecords() khSpawnRefreshMapBlips(true) end scan.waiting = nil scan.index = scan.index + 1 scan.processed = scan.processed + 1 scan.nextAt = os.clock() + 0.4 if type(disableCheckpoint) == 'function' then pcall(disableCheckpoint) end return true end function khSpawnRememberCheckpoint(position, radius) if position == nil then return end local x = tonumber(position.x or position[1]) local y = tonumber(position.y or position[2]) local z = tonumber(position.z or position[3]) if x == nil or y == nil or z == nil then return end khSpawnLastCheckpoint = {x = x, y = y, z = z, radius = tonumber(radius) or 1.0} khSpawnLastCheckpointAt = os.clock() khSpawnCheckpointSerial = (tonumber(khSpawnCheckpointSerial) or 0) + 1 local scan = khSpawnScan if scan == nil or not scan.active or scan.waiting == nil or khIsViceCitySleepMode() then return end local waiting = scan.waiting if waiting.method ~= 'house' and waiting.method ~= 'trailer' then return end if os.clock() - (tonumber(waiting.startedAt) or 0) > 5.8 then return end if (tonumber(khSpawnCheckpointSerial) or 0) <= (tonumber(waiting.checkpointSerial) or 0) then return end local previous = waiting.beforeCheckpoint if type(previous) == 'table' then local dx = x - (tonumber(previous.x) or x) local dy = y - (tonumber(previous.y) or y) local dz = z - (tonumber(previous.z) or z) if dx * dx + dy * dy + dz * dz <= 0.25 then return end end waiting.candidate = {x = x, y = y, z = z, radius = tonumber(radius) or 1.0} khSpawnCheckpointOwner = waiting.candidate khSpawnTryFinishCheckpointScan(scan) end function khSpawnDistanceToZone(entry) if not khSpawnHasCoords(entry) then return nil end local ownZone = khTeamOwnZone if type(ownZone) ~= 'table' or ownZone.active ~= true or not khZoneIsFresh(ownZone) then return nil end local l, r = tonumber(ownZone.left), tonumber(ownZone.right) local u, d = tonumber(ownZone.up), tonumber(ownZone.down) if l == nil or r == nil or u == nil or d == nil then return nil end local minX, maxX = math.min(l, r), math.max(l, r) local minY, maxY = math.min(u, d), math.max(u, d) local x, y = tonumber(entry.x), tonumber(entry.y) local dx = math.max(minX - x, 0, x - maxX) local dy = math.max(minY - y, 0, y - maxY) return math.sqrt(dx * dx + dy * dy) end function khSpawnGetNearestToZone() if khSpawnFeatureEnabled == nil or not khSpawnFeatureEnabled[0] or not khIsScriptEnabled() or khIsViceCitySleepMode() or not zoneActive then return nil end local nearest, bestDistance = nil, nil for _, entry in ipairs(khSpawnRecordsForCurrentServer()) do if khSpawnHasCoords(entry) and not entry.missing then local distance = khSpawnDistanceToZone(entry) if distance ~= nil and (bestDistance == nil or distance < bestDistance) then nearest, bestDistance = entry, distance end end end return nearest, bestDistance end function khSpawnClearMapBlips() -- Больше не ставим блипы на мини-карте спавнов, чтобы GTA не мусорил. end function khSpawnRefreshMapBlips(force) -- Отрисовка выполняется теперь внутри khRenderTeamMapOverlay. end function khSpawnClearNearestBlip() if khSpawnNearestBlip ~= nil then khForgetBlip(khSpawnNearestBlip) khRemoveBlip(khSpawnNearestBlip) end khSpawnNearestBlip = nil khSpawnNearestKey = '' khSpawnNearest = nil end function khSpawnRefreshNearestBlip(force) if not khIsScriptEnabled() or khIsViceCitySleepMode() then khSpawnClearNearestBlip() return end local nearest = khSpawnGetNearestToZone() local key = nearest and tostring(nearest.key or '') or '' if not force and key == tostring(khSpawnNearestKey or '') then khSpawnNearest = nearest return end khSpawnClearNearestBlip() khSpawnNearest = nearest khSpawnNearestKey = key -- Метку на радаре больше не используем: ближайший рисуется на карте команды. end function khSpawnAgeText(entry) local stamp = tonumber(entry and entry.updatedAt) or 0 if stamp <= 0 then return 'нет данных' end local age = math.max(0, os.time() - stamp) if age < 60 then return 'только что' end if age < 3600 then return tostring(math.floor(age / 60)) .. ' мин. назад' end if age < 86400 then return tostring(math.floor(age / 3600)) .. ' ч. назад' end return tostring(math.floor(age / 86400)) .. ' дн. назад' end function khSpawnUpdate(now, spawned, wasSpawned) now = tonumber(now) or os.clock() if khSpawnDialogOpen and now - (tonumber(khSpawnDialogShownAt) or 0) > 12.0 then khSpawnDialogOpen = false khSpawnDialogId = nil end if khSpawnPendingTarget ~= nil and now > (tonumber(khSpawnPendingTargetUntil) or 0) then khSpawnPendingTarget = nil khSpawnPendingTargetUntil = 0 end if khIsViceCitySleepMode() then khSpawnStopHouseScan(true) khSpawnNearestRefreshAt = 0 khSpawnClearMapBlips() khSpawnClearNearestBlip() return end local pendingScan = khSpawnScan if spawned and khSpawnPendingArrival ~= nil and pendingScan ~= nil and pendingScan.waiting ~= nil and pendingScan.waiting.method == 'reconnect' and pendingScan.waiting.record == khSpawnPendingArrival then local selectedAt = tonumber(pendingScan.waiting.selectedAt) or 0 -- Некоторые быстрые реконнекты не успевают отдать переход spawned=false. -- После успешного выбора даём серверу время перенести игрока и сохраняем точку сами. if selectedAt > 0 and now - selectedAt >= 3.0 then khApiResetMovementSession() local entry = khSpawnPendingArrival khSpawnPendingArrival = nil khSpawnArrivalAt = now + 0.4 khSpawnTrailerWait = nil khSpawnTrailerSawInterior = false entry._captureAt = khSpawnArrivalAt entry._scanCapture = true end end if wasSpawned == false and spawned == true and khSpawnPendingArrival ~= nil then local entry = khSpawnPendingArrival khSpawnPendingArrival = nil khSpawnArrivalAt = now + 0.8 khSpawnTrailerWait = nil khSpawnTrailerSawInterior = false local scan = khSpawnScan if scan ~= nil and scan.waiting ~= nil and scan.waiting.method == 'reconnect' and scan.waiting.record == entry then entry._captureAt = khSpawnArrivalAt entry._scanCapture = true elseif tostring(entry.type or '') == 'house' or tostring(entry.type or '') == 'trailer' then entry._captureAt = nil else entry._captureAt = khSpawnArrivalAt entry._scanCapture = false end end for _, entry in ipairs(khSpawnRecords or {}) do if entry._captureAt ~= nil and spawned and now >= tonumber(entry._captureAt) then entry._captureAt = nil local scanned = entry._scanCapture == true entry._scanCapture = nil local saved = khSpawnSaveCurrentCoords(entry) local scan = khSpawnScan if scanned and scan ~= nil and scan.waiting ~= nil and scan.waiting.method == 'reconnect' and scan.waiting.record == entry then if saved then khSpawnFinishScannedHouse(entry.x, entry.y, entry.z) else khSpawnFinishScannedHouse(nil, nil, nil) end end end end local scan = khSpawnScan if scan ~= nil then if not spawned then local waiting = scan.waiting -- При /rec игра на короткое время снимает флаг спавна. Это штатная -- часть сканирования, поэтому не останавливаем очередь на середине. if waiting == nil or waiting.method ~= 'reconnect' then khSpawnStopHouseScan(true) elseif now - (tonumber(waiting.startedAt) or now) >= 24.0 then khSpawnFinishScannedHouse(nil, nil, nil) end elseif scan.waiting ~= nil then local waiting = scan.waiting local timeout = waiting.method == 'reconnect' and 24.0 or 5.8 if now - (tonumber(waiting.startedAt) or now) >= timeout then khSpawnFinishScannedHouse(nil, nil, nil) end elseif now >= (tonumber(scan.nextAt) or 0) and now - (tonumber(scan.lastSentAt) or -100) >= 3.5 then local item = scan.queue[scan.index] if item == nil then khSpawnStopHouseScan(true) nakhodkaNotify(sName .. '\xD1\xEA\xE0\xED\x20\xF1\xEF\xE0\xE2\xED\xEE\xE2\x20\xE7\xE0\xE2\xE5\xF0\xF8\xE5\xED\x2E', -1, 'success', 3) else local entry = item.record scan.waiting = { record = entry, method = item.method, startedAt = now, checkpointSerial = tonumber(khSpawnCheckpointSerial) or 0, beforeCheckpoint = khSpawnLastCheckpoint, candidate = nil, confirmed = false } scan.lastSentAt = now scan.nextAt = now + 3.5 if item.method == 'house' and type(sampSendChat) == 'function' then sampSendChat('/findihouse ' .. tostring(math.floor(tonumber(entry.houseId) or 0))) elseif item.method == 'trailer' and type(sampSendChat) == 'function' then sampSendChat('/findtrailer') elseif item.method == 'reconnect' then if not khSpawnRequestReconnect(entry) then khSpawnFinishScannedHouse(nil, nil, nil) end else khSpawnFinishScannedHouse(nil, nil, nil) end end end end if now >= (tonumber(khSpawnNearestRefreshAt) or 0) then khSpawnNearestRefreshAt = now + 0.65 khSpawnRefreshNearestBlip(false) end end function khSpawnDisplayName(entry) local label = tostring(entry and entry.rawName or 'спавн') if entry ~= nil and entry.type == 'house' and tonumber(entry.houseId) ~= nil then label = 'Дом #' .. tostring(entry.houseId) end return label end function khSpawnRenderTeamMapAll(mx, my, size) if khSpawnMapEnabled == nil or not khSpawnMapEnabled[0] or (khSpawnFeatureEnabled ~= nil and khSpawnFeatureEnabled[0]) or not khIsScriptEnabled() or khIsViceCitySleepMode() then return end local blue = khTeamArgb(255, 55, 150, 255) local io = imguiReady and imgui ~= nil and imgui.GetIO ~= nil and imgui.GetIO() or nil local mouse = io and io.MousePos or nil local clicked = io and io.MouseClicked and io.MouseClicked[0] local mouseX = mouse ~= nil and tonumber(mouse.x) or nil local mouseY = mouse ~= nil and tonumber(mouse.y) or nil for _, entry in ipairs(khSpawnRecordsForCurrentServer()) do if khSpawnHasCoords(entry) and not entry.missing then local sx, sy = khTeamMapWorldToScreen(entry.x, entry.y, mx, my, size) khTeamMapDrawCircle(sx, sy, 4.5, blue, khSpawnDisplayName(entry), blue) if clicked and mouseX ~= nil and mouseY ~= nil then local dx, dy = mouseX - sx, mouseY - sy if dx * dx + dy * dy <= 18 * 18 then khSpawnRequestReconnect(entry) return end end end end end function khSpawnRenderTeamMapNearest(mx, my, size) if khSpawnMapEnabled == nil or not khSpawnMapEnabled[0] or khSpawnFeatureEnabled == nil or not khSpawnFeatureEnabled[0] then return end local entry = khSpawnNearest or khSpawnGetNearestToZone() if entry == nil or khIsViceCitySleepMode() then return end local sx, sy = khTeamMapWorldToScreen(entry.x, entry.y, mx, my, size) local blue = khTeamArgb(255, 55, 150, 255) khTeamMapDrawCircle(sx, sy, 6.2, blue, khSpawnDisplayName(entry), blue) if not imguiReady or imgui == nil or imgui.GetIO == nil then return end local io = imgui.GetIO() local mouse = io and io.MousePos or nil local clicked = io and io.MouseClicked and io.MouseClicked[0] local mouseX = mouse ~= nil and tonumber(mouse.x) or nil local mouseY = mouse ~= nil and tonumber(mouse.y) or nil if clicked and mouseX ~= nil and mouseY ~= nil then local dx, dy = mouseX - sx, mouseY - sy if dx * dx + dy * dy <= 18 * 18 then khSpawnRequestReconnect(entry) end end end function khRenderSpawnsWindow() if khSpawnWindowOpen == nil or not khSpawnWindowOpen[0] then return end local accent = khThemeColor(nil, 'accentText') local muted = khThemeColor(nil, 'muted') local green = imgui.ImVec4(0.35, 0.95, 0.48, 1.00) local red = imgui.ImVec4(1.00, 0.32, 0.38, 1.00) local flags = 0 if imgui.WindowFlags.NoCollapse ~= nil then flags = flags + imgui.WindowFlags.NoCollapse end if imgui.WindowFlags.NoSavedSettings ~= nil then flags = flags + imgui.WindowFlags.NoSavedSettings end imgui.SetNextWindowSize(imgui.ImVec2(720, 520), imgui.Cond.FirstUseEver or imgui.Cond.Always) if imgui.Begin(khText('Спавны') .. '##kh_spawns_window', khSpawnWindowOpen, flags) then imgui.TextWrapped(khText('Список мест спавна. Зелёные точки знают координаты, красные ещё не просканированы.')) imgui.Separator() local scan = khSpawnScan if scan ~= nil then local total = #(scan.queue or {}) local current = scan.waiting and scan.waiting.record or scan.queue[scan.index] imgui.TextColored(accent, khText('Сканирую: ' .. tostring(current and current.rawName or 'спавн') .. ' - ' .. tostring((tonumber(scan.processed) or 0) + 1) .. '/' .. tostring(total))) imgui.SameLine() if imgui.Button(khText('Стоп') .. '##kh_spawns_stop', imgui.ImVec2(96, 28)) then khSpawnStopHouseScan(false) end else if imgui.Button(khText('Просканировать спавны') .. '##kh_spawns_scan', imgui.ImVec2(208, 28)) then khSpawnBeginHouseScan(false) end imgui.SameLine() if imgui.Button(khText('Пересканировать') .. '##kh_spawns_rescan', imgui.ImVec2(136, 28)) then khSpawnBeginHouseScan(true) end end imgui.SameLine() if imgui.Button(khText('Забыть координаты') .. '##kh_spawns_forget', imgui.ImVec2(166, 28)) then for _, entry in ipairs(khSpawnRecordsForCurrentServer()) do entry.x, entry.y, entry.z, entry.updatedAt = nil, nil, nil, 0 entry.missing = false end khSaveSpawnRecords() khSpawnRefreshMapBlips(true) khSpawnRefreshNearestBlip(true) end imgui.SameLine() if imgui.Button(khText('Сбросить спавны') .. '##kh_spawns_reset', imgui.ImVec2(150, 28)) then if khSpawnScan ~= nil then khSpawnStopHouseScan(false) end khSpawnRecords = {} khSpawnDialogEntries = {} khSaveSpawnRecords() khSpawnClearNearestBlip() khSpawnRefreshMapBlips(true) khSpawnRefreshNearestBlip(true) nakhodkaNotify(sName .. 'Спавны сброшены. Перезайди на сервер или открой /rec, чтобы собрать список заново.', -1, 'success', 4) end imgui.Separator() imgui.BeginChild('##kh_spawns_list', imgui.ImVec2(0, 0), true) local spawnRecordsView = khSpawnRecordsForCurrentServer() if #spawnRecordsView == 0 then imgui.TextColored(muted, khText('Открой /rec вручную, чтобы Находка запомнила места спавна.')) else for index, entry in ipairs(spawnRecordsView) do local known = khSpawnHasCoords(entry) and not entry.missing local rowWidth = imgui.GetContentRegionAvail().x imgui.Columns(2, '##kh_spawns_row_' .. tostring(index), false) imgui.SetColumnWidth(0, math.max(220, rowWidth - 94)) imgui.TextColored(known and green or red, known and 'o' or 'x') imgui.SameLine() local distance = khSpawnDistanceToZone(entry) local suffix = known and (' | ' .. (distance and string.format('%.0f м до зоны', distance) or 'зона не активна') .. ' | ' .. khSpawnAgeText(entry)) or ' | координаты неизвестны' imgui.TextWrapped(khText(tostring(entry.rawName or 'Спавн') .. suffix)) imgui.NextColumn() if imgui.Button(khText('Рекнуться') .. '##kh_spawns_rec_' .. tostring(index), imgui.ImVec2(84, 24)) then khSpawnRequestReconnect(entry) end imgui.NextColumn() imgui.Columns(1) end end imgui.EndChild() end imgui.End() end local khViceCitySleepState = false function khSyncOwnTeamZone(active, l, u, r, d, createdAt) if active then local zl,zu,zr,zd=tonumber(l or left)or 0,tonumber(u or up)or 0,tonumber(r or right)or 0,tonumber(d or down)or 0 local stamp=khZoneEpoch(createdAt) local old=khTeamOwnZone or {} if not stamp and old.active and tonumber(old.left)==zl and tonumber(old.up)==zu and tonumber(old.right)==zr and tonumber(old.down)==zd then stamp=khZoneCreatedAt(old) end if not stamp and mainCfg and mainCfg.LastZone then stamp=khZoneEpoch(mainCfg.LastZone.savedAt) end if not stamp then stamp=os.time() end khTeamOwnZone={active=true,left=zl,up=zu,right=zr,down=zd,createdAt=stamp,expiresAt=stamp+khZoneLifetimeSeconds} else khTeamOwnZone={active=false} end end -- \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 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 khIsMainPointInTeamZone(point) if type(point) ~= 'table' then return false end local x = tonumber(point[1]) local y = tonumber(point[2]) if x == nil or y == nil then return false end for token, member in pairs(khTeamMembers or {}) do local online = member.online ~= false local zone = type(member.zone) == 'table' and member.zone or nil if online and zone and khTeamZoneIsFresh(member,token) then local l = tonumber(zone.left) local r = tonumber(zone.right) local u = tonumber(zone.up) local d = tonumber(zone.down) if l ~= nil and r ~= nil and u ~= nil and d ~= nil then local minX, maxX = math.min(l, r), math.max(l, r) local minY, maxY = math.min(u, d), math.max(u, d) if x >= minX and x <= maxX and y >= minY and y <= maxY then return true end end end end return false end function khIsMainPointInVisibleZone(point) return khIsMainPointInActiveZone(point) or khIsMainPointInTeamZone(point) 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 function khResetMainTreasureChecked() khMainTreasureChecked = {} end function khShouldShowMainTreasurePoint(index, point, px, py, pz) if not khIsScriptEnabled() or khIsNoTreasureServer() then return false end if khMainTreasureChecked[index] then return false end if khOnlyInZone[0] and not khIsMainPointInVisibleZone(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 checkRadius = tonumber(khPointCheckRadius[0]) or 12 if checkRadius > 0 and khFeatureEnabled('pointcheck') and (dx * dx + dy * dy) <= checkRadius * checkRadius then khMainTreasureChecked[index] = true return false end local radius = tonumber(khPointDisplayRadius[0]) or 300 return (dx * dx + dy * dy) <= radius * radius end function khUpdateMainTreasureCheckedFast(px, py) if not khIsScriptEnabled() or khIsNoTreasureServer() or not khFeatureEnabled('pointcheck') or not px or not py then return false end local checkRadius = tonumber(khPointCheckRadius[0]) or 12 if checkRadius <= 0 then return false end local checkRadius2 = checkRadius * checkRadius local changed = false for index, point in ipairs(khMainTreasurePoints) do if not khMainTreasureChecked[index] and (not khOnlyInZone[0] or khIsMainPointInActiveZone(point)) then local x = tonumber(point[1]) or 0 local y = tonumber(point[2]) or 0 local dx, dy = px - x, py - y if (dx * dx + dy * dy) <= checkRadius2 then khMainTreasureChecked[index] = true changed = true end end end return changed 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 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 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 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) if not khIsFiniteNumber(x) or not khIsFiniteNumber(y) or not khIsFiniteNumber(z) then return false end if type(isPointOnScreen) ~= 'function' then return true end local ok, visible = pcall(isPointOnScreen, x, y, z, 0.35) if ok then return visible ~= false end return true end function khWorldToScreen(x, y, z) if kh3DWorldToScreenDisabled then return nil, nil end if type(convert3DCoordsToScreen) ~= 'function' or not khWorldPointOnScreen(x, y, z) then return nil, nil end local ok, sx, sy = pcall(convert3DCoordsToScreen, x, y, z) if not ok then kh3DWorldToScreenDisabled = true return nil, nil end if not khIsFiniteNumber(sx) or not khIsFiniteNumber(sy) then return nil, nil end if type(getScreenResolution) == 'function' then local sw, sh = getScreenResolution() if sw and sh and (sx < -80 or sy < -80 or sx > sw + 80 or sy > sh + 80) then return nil, nil end end return sx, sy end function khDrawScreenLine(x1, y1, x2, y2, color, width) if kh3DRenderLineDisabled or type(renderDrawLine) ~= 'function' then return end if not khIsFiniteNumber(x1) or not khIsFiniteNumber(y1) or not khIsFiniteNumber(x2) or not khIsFiniteNumber(y2) then return end if type(getScreenResolution) == 'function' then local sw, sh = getScreenResolution() if sw and sh then if x1 < -80 or x1 > sw + 80 or x2 < -80 or x2 > sw + 80 or y1 < -80 or y1 > sh + 80 or y2 < -80 or y2 > sh + 80 then return end end end local ok = pcall(renderDrawLine, x1, y1, x2, y2, width or 2, color) if not ok then kh3DRenderLineDisabled = true end end function khDrawWorldCircle(x, y, z, radius, color, width) radius = tonumber(radius) or 1.5 if radius < 0.2 then radius = 0.2 end if radius > 6.0 then radius = 6.0 end if not khWorldPointOnScreen(x, y, z) then return end local segments = 14 local prevX, prevY = nil, nil local maxJump = 180 for i = 0, segments do local angle = (math.pi * 2) * (i / segments) local sx, sy = khWorldToScreen(x + math.cos(angle) * radius, y + math.sin(angle) * radius, z) if sx and sy then if prevX and prevY and math.abs(sx - prevX) <= maxJump and math.abs(sy - prevY) <= maxJump then khDrawScreenLine(prevX, prevY, sx, sy, color, width or 2) end prevX, prevY = sx, sy else prevX, prevY = nil, nil end end end function khCreate3DPickup(model, x, y, z) if type(createUser3dMarker) ~= 'function' then return nil end local ok, marker = pcall(createUser3dMarker, x, y, z + 1.5, 4) if ok and type(marker) == 'number' and marker >= 0 then return marker end return nil end function khRemove3DPickup(handle) if type(handle) == 'table' then handle = handle.handle end if type(handle) ~= 'number' or handle < 0 then return end if type(removeUser3dMarker) == 'function' then pcall(removeUser3dMarker, handle) end end function khQueue3DPickupRemove(data) if data == nil then return end if kh3DPickupClearQueue == nil then kh3DPickupClearQueue = {} end kh3DPickupClearQueue[#kh3DPickupClearQueue + 1] = data end function khHasNative3DCleanupPending() return kh3DPickupClearQueue ~= nil and #kh3DPickupClearQueue > 0 end function khProcess3DPickupClearQueue(maxPerFrame) if kh3DClearingPickups then return not khHasNative3DCleanupPending() end kh3DClearingPickups = true local limit = tonumber(maxPerFrame) or 10 local processed = 0 while kh3DPickupClearQueue ~= nil and #kh3DPickupClearQueue > 0 and processed < limit do local data = table.remove(kh3DPickupClearQueue, 1) khRemove3DPickup(data) processed = processed + 1 end local pending = khHasNative3DCleanupPending() if pending then local nowClock = os.clock() if (kh3DNativePauseUntil or 0) < nowClock + 0.06 then kh3DNativePauseUntil = nowClock + 0.06 end end kh3DClearingPickups = false return not pending end function khQueueClear3DPickups() for key, data in pairs(kh3DPickupMarkers or {}) do khQueue3DPickupRemove(data) kh3DPickupMarkers[key] = nil end local nowClock = os.clock() if (kh3DNativePauseUntil or 0) < nowClock + 0.80 then kh3DNativePauseUntil = nowClock + 0.80 end end khClear3DPickups = function(maxPerFrame) khQueueClear3DPickups() return khProcess3DPickupClearQueue(maxPerFrame or 4) end function kh3DRequestMarkerType(value) local nextType = tonumber(value) == 1 and 1 or 0 if kh3DPendingMarkerType == nextType or (kh3DPendingMarkerType == nil and kh3DMarkerType[0] == nextType) then return false end kh3DPendingMarkerType = nextType kh3DForceClear = true kh3DSwitchPending = true kh3DMainDrawList = {} kh3DDopDrawList = {} kh3DLastBuildAt = 0 khQueueClear3DPickups() khProcess3DPickupClearQueue(3) local nowClock = os.clock() kh3DSwitchCooldownUntil = nowClock + 1.10 kh3DNativePauseUntil = nowClock + 1.10 if nextType == 1 then nakhodkaNotify('3D-маркеры переключаются на красивые круги.', -1, 'success', 2) else nakhodkaNotify('3D-маркеры переключаются на красивые круги.', -1, 'success', 2) end return true end function kh3DApplyPendingMarkerType(nowClock) if kh3DPendingMarkerType == nil then return false end nowClock = tonumber(nowClock) or os.clock() khProcess3DPickupClearQueue(3) kh3DMainDrawList = {} kh3DDopDrawList = {} kh3DLastBuildAt = 0 if khHasNative3DCleanupPending() or nowClock < (kh3DSwitchCooldownUntil or 0) or nowClock < (kh3DNativePauseUntil or 0) then return true end kh3DMarkerType[0] = kh3DPendingMarkerType kh3DPendingMarkerType = nil kh3DForceClear = false kh3DSwitchPending = false kh3DActiveMarkerType = kh3DMarkerType[0] kh3DSwitchCooldownUntil = nowClock + 0.55 kh3DNativePauseUntil = nowClock + 0.55 khSaveMainSettings() return true end function khMarkerLineVisible(x, y, z) if not kh3DMarkersAntiWh[0] then return true end local fromX, fromY, fromZ = nil, nil, nil if type(getActiveCameraCoordinates) == 'function' then fromX, fromY, fromZ = getActiveCameraCoordinates() end if not fromX or not fromY or not fromZ then fromX, fromY, fromZ = getCharCoordinates(PLAYER_PED) if fromZ then fromZ = fromZ + 0.8 end end if not fromX or not fromY or not fromZ then return true end if type(processLineOfSight) == 'function' then local ok, hit = pcall(processLineOfSight, fromX, fromY, fromZ, x, y, z + 0.5, true, false, false, true, false, false, false) if ok then return hit ~= true end end if type(isLineOfSightClear) == 'function' then local ok, clear = pcall(isLineOfSightClear, fromX, fromY, fromZ, x, y, z + 0.5, true, false, false, true, false, false, false) if ok then return clear ~= false end end return true end function khTrim3DList(list, limit) table.sort(list, function(a, b) return (a.dist2 or 0) < (b.dist2 or 0) end) while #list > limit do list[#list] = nil end end function khRebuild3DMarkerCache(px, py, pz) kh3DMainDrawList = {} kh3DDopDrawList = {} local markerType = tonumber(kh3DMarkerType[0]) or 0 local maxDistance if markerType == 1 then maxDistance = 30 else maxDistance = math.max(30, math.min(300, tonumber(kh3DMarkerDistance[0]) or 100)) end local maxDistance2 = maxDistance * maxDistance for index, point in ipairs(khMainTreasurePoints) do if not khOnlyInZone[0] or khIsMainPointInVisibleZone(point) then local x = tonumber(point[1]) or 0 local y = tonumber(point[2]) or 0 local z = tonumber(point[3]) or 0 local dx, dy = px - x, py - y local dist2 = dx * dx + dy * dy if dist2 <= maxDistance2 then table.insert(kh3DMainDrawList, {index = index, x = x, y = y, z = z, dist2 = dist2}) end end end for _, entry in ipairs(kh3DMainDrawList) do entry.visible = (entry.dist2 or 0) <= 4.0 or khMarkerLineVisible(entry.x, entry.y, entry.z) end if khFeatureEnabled('dopki') then for index, point in ipairs(khAdditionalPoints) do local x, y, z = tonumber(point.x), tonumber(point.y), tonumber(point.z) if x and y and z then local dx, dy = px - x, py - y local dist2 = dx * dx + dy * dy if dist2 <= maxDistance2 then table.insert(kh3DDopDrawList, {index = index, x = x, y = y, z = z, dist2 = dist2}) end end end for _, entry in ipairs(kh3DDopDrawList) do entry.visible = (entry.dist2 or 0) <= 4.0 or khMarkerLineVisible(entry.x, entry.y, entry.z) end end end function khUpdate3DListDistances(list, px, py) if type(list) ~= 'table' or not px or not py then return end for _, entry in ipairs(list) do local dx, dy = px - (tonumber(entry.x) or 0), py - (tonumber(entry.y) or 0) entry.dist2 = dx * dx + dy * dy end end function khSet3DPickupWanted(wanted, key, model, x, y, z) wanted[key] = true local current = kh3DPickupMarkers[key] local currentModel = type(current) == 'table' and current.model or nil if current ~= nil and currentModel ~= model then khQueue3DPickupRemove(current) kh3DPickupMarkers[key] = nil local nowClock = os.clock() if (kh3DNativePauseUntil or 0) < nowClock + 0.45 then kh3DNativePauseUntil = nowClock + 0.45 end return end if current == nil then local handle = khCreate3DPickup(model, x, y, z) if handle ~= nil then kh3DPickupMarkers[key] = {handle = handle, model = model} else kh3DPickupMarkers[key] = nil end end end function khUpdate3DPickups() khProcess3DPickupClearQueue(10) if khHasNative3DCleanupPending() then return end local nowClock = os.clock() if nowClock < (kh3DSwitchCooldownUntil or 0) or nowClock < (kh3DNativePauseUntil or 0) then return end -- Если прямо сейчас идёт переключение - ничего не создаём и не удаляем, ждём следующий кадр. if kh3DForceClear or kh3DSwitchPending then return end local wanted = {} if type(createUser3dMarker) ~= 'function' then khClear3DPickups() return end -- \xd1\xf2\xf0\xe5\xeb\xea\xe0-\xf3\xea\xe0\xe7\xe0\xf2\xe5\xeb\xfc \xed\xe0\xe4 \xea\xe0\xe6\xe4\xfb\xec \xea\xeb\xe0\xe4\xee\xec (User 3D Marker, \xf2\xe8\xef 4, \xed\xe5 \xe8\xf1\xf7\xe5\xe7\xe0\xe5\xf2). -- \xcf\xf0\xee\xe2\xe5\xf0\xea\xf3 visible \xed\xe5 \xef\xf0\xe8\xec\xe5\xed\xff\xe5\xec - \xf1\xf2\xf0\xe5\xeb\xea\xe0 \xfd\xf2\xee \xf3\xea\xe0\xe7\xe0\xf2\xe5\xeb\xfc, \xe5\xb8 \xed\xe0\xe4\xee \xe2\xe8\xe4\xe5\xf2\xfc \xe4\xe0\xe6\xe5 \xe7\xe0 \xf0\xe5\xeb\xfc\xe5\xf4\xee\xec. -- Сначала собираем список нужных ключей... local plan = {} for _, entry in ipairs(kh3DMainDrawList) do local key = 'main_' .. tostring(entry.index) wanted[key] = true plan[#plan + 1] = {key = key, x = entry.x, y = entry.y, z = entry.z} end for _, entry in ipairs(kh3DDopDrawList) do local key = 'dop_' .. tostring(entry.index) wanted[key] = true plan[#plan + 1] = {key = key, x = entry.x, y = entry.y, z = entry.z} end -- Сначала убираем лишние маркеры через очередь, без create/remove в один кадр. for key, data in pairs(kh3DPickupMarkers) do if not wanted[key] then khQueue3DPickupRemove(data) kh3DPickupMarkers[key] = nil end end if khHasNative3DCleanupPending() then local nowClock = os.clock() if (kh3DNativePauseUntil or 0) < nowClock + 0.12 then kh3DNativePauseUntil = nowClock + 0.12 end return end -- Потом создаем новые для актуального списка. for _, item in ipairs(plan) do khSet3DPickupWanted(wanted, item.key, 4, item.x, item.y, item.z) end end function khRender3DWorldMarkers() local nowClock = os.clock() local markersEnabled = khIsScriptEnabled() and khFeatureEnabled('markers3d') and not khIsNoTreasureServer() and kh3DMarkersEnabled ~= nil and kh3DMarkersEnabled[0] if not markersEnabled then if kh3DForceClear or kh3DSwitchPending or next(kh3DPickupMarkers or {}) ~= nil or khHasNative3DCleanupPending() then kh3DForceClear = false kh3DSwitchPending = false khClear3DPickups(4) end if #kh3DMainDrawList > 0 then kh3DMainDrawList = {} end if #kh3DDopDrawList > 0 then kh3DDopDrawList = {} end kh3DActiveMarkerType = nil return end -- Отложенная очистка из imgui-кнопок (безопасно в основном потоке) khProcess3DPickupClearQueue(4) if kh3DApplyPendingMarkerType(nowClock) then return end -- Любое переключение типа или принудительная очистка сначала снимает все нативные -- маркеры и выдерживает паузу (kh3DSwitchCooldownUntil), чтобы createUser3dMarker -- не вызывался в тот же кадр, что удаление предыдущих - иначе краш cimguidx9.dll. if kh3DForceClear or kh3DSwitchPending then kh3DForceClear = false kh3DSwitchPending = false khClear3DPickups() kh3DMainDrawList = {} kh3DDopDrawList = {} kh3DLastBuildAt = 0 kh3DActiveMarkerType = nil if kh3DSwitchCooldownUntil < nowClock + 0.45 then kh3DSwitchCooldownUntil = nowClock + 0.45 end return end -- Ещё не закончился период остывания после переключения - ждём. if nowClock < (kh3DSwitchCooldownUntil or 0) or nowClock < (kh3DNativePauseUntil or 0) or khHasNative3DCleanupPending() then return end local px, py, pz = getCharCoordinates(PLAYER_PED) if not px or not py then khClear3DPickups() return end local markerType = tonumber(kh3DMarkerType[0]) or 0 if kh3DActiveMarkerType ~= markerType then -- Тип поменялся не через кнопку (например, конфиг): обрабатываем как переключение. khClear3DPickups() kh3DMainDrawList = {} kh3DDopDrawList = {} kh3DLastBuildAt = 0 kh3DActiveMarkerType = markerType if kh3DSwitchCooldownUntil < nowClock + 0.45 then kh3DSwitchCooldownUntil = nowClock + 0.45 end return end if nowClock - kh3DLastBuildAt >= kh3DScanInterval then kh3DLastBuildAt = nowClock khRebuild3DMarkerCache(px, py, pz or 0) else khUpdate3DListDistances(kh3DMainDrawList, px, py) khUpdate3DListDistances(kh3DDopDrawList, px, py) end local checkRadius = tonumber(khPointCheckRadius[0]) or 12 if checkRadius > 0 then local checkRadius2 = checkRadius * checkRadius for _, entry in ipairs(kh3DMainDrawList) do if (entry.dist2 or 0) <= checkRadius2 then khMainTreasureChecked[entry.index] = true end end end local radius = math.max(0.5, math.min(5.0, tonumber(kh3DMarkerRadius10[0]) or 1.5)) if kh3DMarkerType[0] == 1 then -- Тип "Стрелки": и основные, и доп точки получают белую стрелку khUpdate3DPickups() return end -- Тип "Круги": рисуем кольца на земле только после полного снятия нативных стрелок. if next(kh3DPickupMarkers or {}) ~= nil or khHasNative3DCleanupPending() then khClear3DPickups(10) return end if type(convert3DCoordsToScreen) ~= 'function' or type(renderDrawLine) ~= 'function' then return end local normalColor = khArgb(235, kh3DNormalR[0], kh3DNormalG[0], kh3DNormalB[0]) local checkedColor = khArgb(235, kh3DCheckedR[0], kh3DCheckedG[0], kh3DCheckedB[0]) for _, entry in ipairs(kh3DMainDrawList) do if entry.visible ~= false then local color = khMainTreasureChecked[entry.index] and checkedColor or normalColor khDrawWorldCircle(entry.x, entry.y, entry.z + 0.08, radius, color, 2) end end for _, entry in ipairs(kh3DDopDrawList) do if entry.visible ~= false then khDrawWorldCircle(entry.x, entry.y, entry.z + 0.12, radius * 1.15, khRainbowColor(nowClock * 0.18 + entry.index * 0.137), 3) end end end -- \xcd\xe0\xf1\xf2\xf0\xee\xe9\xea\xe8 \xea\xee\xed\xf4\xe8\xe3\xe0 \xe4\xeb\xff \xf1\xee\xf5\xf0\xe0\xed\xe5\xed\xe8\xff \xef\xee\xf1\xeb\xe5\xe4\xed\xe5\xe9 \xe7\xee\xed\xfb mainCfg = inicfg.load({ LastZone = { saved = false, left = 0, up = 0, right = 0, down = 0, savedAt = 0 }, Settings = { enabled = true, onlyInZone = false, disableZoneBlink = true, pointDisplayRadius = 300, pointCheckRadius = 12, pointIcon = 56, autoUpdate = true, fixedDigCursor = true, unloadKey = 0 }, Notifications = { enabled = true, map = true, zones = true, points = true, drops = true, team = true, updates = true, companion = true, system = true }, Marks = { lastUpdate = '', count = 0 }, Api = { installId = '' }, 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 = '', mapEnabled = true, mapTransparency = 0, showNearby = false }, Spawns = { enabled = true, count = 0 }, Tyan = { enabled = false, x = 240, y = 500, size = 136, variant = 0, customId = '' } }, config_file) if mainCfg.Settings == nil then mainCfg.Settings = { enabled = true, onlyInZone = false, disableZoneBlink = true, pointDisplayRadius = 300, pointCheckRadius = 12, pointIcon = 56, autoUpdate = true, fixedDigCursor = true, unloadKey = 0 } end if mainCfg.Settings.autoUpdateDefaultMigrated == nil then mainCfg.Settings.autoUpdate = true mainCfg.Settings.autoUpdateDefaultMigrated = true pcall(inicfg.save, mainCfg, config_file) end if mainCfg.Hud == nil then mainCfg.Hud = {enabled = true, x = 22, y = 220} end if mainCfg.Notifications == nil then mainCfg.Notifications = {enabled = true, map = true, zones = true, points = true, drops = true, team = true, updates = true, companion = true, system = true} end if mainCfg.Marks == nil then mainCfg.Marks = {lastUpdate = '', count = 0} end if mainCfg.Api == nil then mainCfg.Api = {installId = ''} end khMarksLastUpdateText = tostring(mainCfg.Marks.lastUpdate or '') 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 = '', mapEnabled = true, mapTransparency = 0, showNearby = false } end if mainCfg.Spawns == nil then mainCfg.Spawns = {enabled = true, mapEnabled = false, count = 0} end if mainCfg.Tyan == nil then mainCfg.Tyan = { enabled = false, x = 240, y = 500, size = 136, variant = 0, customId = '' } 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 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 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) khDisableZoneBlink[0] = khCfgBool(mainCfg.Settings.disableZoneBlink, true) khZoneOpsEnabled[0] = khCfgBool(mainCfg.Settings.zoneOps, true) khPointDisplayRadius[0] = khClampInt(mainCfg.Settings.pointDisplayRadius, 300, 50, 2000) khPointCheckRadius[0] = khClampInt(mainCfg.Settings.pointCheckRadius, 12, 0, 100) khPointIcon[0] = khClampInt(mainCfg.Settings.pointIcon, 56, 1, 63) khAutoUpdateEnabled[0] = khCfgBool(mainCfg.Settings.autoUpdate, true) khFixedDigCursorEnabled[0] = khFeatureEnabled('digCursor') and khCfgBool(mainCfg.Settings.fixedDigCursor, true) or false khHideServerIcons[0] = khCfgBool(mainCfg.Settings.hideServerIcons, false) khNotificationsEnabled[0] = khCfgBool(mainCfg.Notifications.enabled, true) for category, toggle in pairs(khNotifyCategories) do toggle[0] = khCfgBool(mainCfg.Notifications[category], true) end khUnloadKey = math.floor((tonumber(mainCfg.Settings.unloadKey) or 0) + 0.5) if khUnloadKey < 0 or khUnloadKey > 254 or (khUnloadKey > 0 and khUnloadKey < 5) then khUnloadKey = 0 end 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] = khFeatureEnabled('markers3d') and khCfgBool(kh3DCfg.enabled, false) or false kh3DMarkersAntiWh[0] = khCfgBool(kh3DCfg.antiWh, 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] = khFeatureEnabled('team') and khCfgBool(khTeamCfg.enabled, false) or false khTeamMapEnabled[0] = khFeatureEnabled('teamMap') and khCfgBool(khTeamCfg.mapEnabled, true) or false khTeamMapHoldMode[0] = khCfgBool(khTeamCfg.mapHold, false) khTeamMapTransparency[0] = khClampInt(khTeamCfg.mapTransparency, 0, 0, 80) khTeamShowNearby[0] = khCfgBool(khTeamCfg.showNearby, false) khTeamMapKey = math.floor((tonumber(khTeamCfg.mapKey) or 0x47) + 0.5) if khTeamMapKey < 1 or khTeamMapKey > 254 then khTeamMapKey = 0x47 end khTeamMapPos.x = tonumber(khTeamCfg.mapX) khTeamMapPos.y = tonumber(khTeamCfg.mapY) khTeamMapSize = tonumber(khTeamCfg.mapSize) or 0 khSpawnCfg = mainCfg.Spawns or {} khSpawnFeatureEnabled[0] = khCfgBool(khSpawnCfg.enabled, true) khSpawnMapEnabled[0] = khCfgBool(khSpawnCfg.mapEnabled, false) khLoadSpawnRecords() khTeam.host = 'nakhodka.fun' khTeam.port = 443 khTeamPort[0] = khTeam.port khTeam.token = tostring(khTeamCfg.token or '') khTeam.manualServer = '' khTyanCfg = mainCfg.Tyan or {} khTyanEnabled[0] = khFeatureEnabled('companion') and khCfgBool(khTyanCfg.enabled, false) or false khTyanPos.x = tonumber(khTyanCfg.x) or 240 khTyanPos.y = tonumber(khTyanCfg.y) or 500 khTyanSize = khClampInt(khTyanCfg.size, 260, 200, 800) khTyanSizeInput[0] = khTyanSize khTyanVariant = khClampInt(khTyanCfg.variant, 0, 0, 1) khLoadCustomCompanions() local savedCustomId = khTeamCleanText(khTyanCfg.customId or '', 48) if savedCustomId ~= '' and khCustomFind(savedCustomId) ~= nil then khCustomActiveId = savedCustomId khCustomSelectedId = savedCustomId khCustomLoadGlobals(khCustomFind(savedCustomId)) khCustomLoadEditor(khCustomFind(savedCustomId)) end 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 khZoneOpsEnabled ~= nil and not khZoneOpsEnabled[0] then khDisableZoneBlink[0] = false if type(removeGangZone) == 'function' then pcall(removeGangZone, 610) end end if mainCfg.Settings == nil then mainCfg.Settings = {} end if mainCfg.Hud == nil then mainCfg.Hud = {} end if mainCfg.Notifications == nil then mainCfg.Notifications = {} 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 if mainCfg.Spawns == nil then mainCfg.Spawns = {} end mainCfg.Settings.enabled = khScriptEnabled[0] mainCfg.Settings.onlyInZone = khOnlyInZone[0] mainCfg.Settings.disableZoneBlink = khDisableZoneBlink[0] and true or false 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.Settings.fixedDigCursor = khFeatureEnabled('digCursor') and khFixedDigCursorEnabled[0] and true or false mainCfg.Settings.hideServerIcons = khHideServerIcons[0] and true or false mainCfg.Notifications.enabled = khNotificationsEnabled[0] and true or false for category, toggle in pairs(khNotifyCategories) do mainCfg.Notifications[category] = toggle[0] and true or false end mainCfg.Settings.unloadKey = math.floor((tonumber(khUnloadKey) or 0) + 0.5) 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 = khFeatureEnabled('markers3d') and kh3DMarkersEnabled[0] or false mainCfg.ThreeDMarkers.antiWh = kh3DMarkersAntiWh[0] 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 = khFeatureEnabled('team') and khTeamEnabled[0] or false mainCfg.Team.host = khTeam.host mainCfg.Team.port = khTeamPort[0] mainCfg.Team.token = khTeam.token mainCfg.Team.manualServer = '' mainCfg.Team.mapEnabled = khFeatureEnabled('teamMap') and khTeamMapEnabled[0] and true or false mainCfg.Team.mapHold = khTeamMapHoldMode ~= nil and khTeamMapHoldMode[0] and true or false mainCfg.Team.mapTransparency = khClampInt(khTeamMapTransparency and khTeamMapTransparency[0], 0, 0, 80) mainCfg.Team.showNearby = khTeamShowNearby ~= nil and khTeamShowNearby[0] and true or false mainCfg.Team.mapKey = math.floor((tonumber(khTeamMapKey) or 0x47) + 0.5) mainCfg.Team.mapX = math.floor((tonumber(khTeamMapPos and khTeamMapPos.x) or 0) + 0.5) mainCfg.Team.mapY = math.floor((tonumber(khTeamMapPos and khTeamMapPos.y) or 0) + 0.5) mainCfg.Team.mapSize = math.floor((tonumber(khTeamMapSize) or 0) + 0.5) mainCfg.Spawns.enabled = khSpawnFeatureEnabled ~= nil and khSpawnFeatureEnabled[0] and true or false mainCfg.Spawns.mapEnabled = khSpawnMapEnabled ~= nil and khSpawnMapEnabled[0] and true or false mainCfg.Tyan.enabled = khFeatureEnabled('companion') and khTyanEnabled[0] or false 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 mainCfg.Tyan.customId = khCustomActiveId ~= nil and tostring(khCustomActiveId) or '' khCustomSyncGlobals() inicfg.save(mainCfg, config_file) khSaveCustomCompanions() end khTeamSaveSettings = function() khTeam.port = 443 khTeamPort[0] = 443 khSaveMainSettings() end function khSaveHudPosition() if mainCfg.Hud == nil then mainCfg.Hud = {} end if mainCfg.Notifications == nil then mainCfg.Notifications = {} 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 = khFeatureEnabled('markers3d') and kh3DMarkersEnabled[0] or false mainCfg.ThreeDMarkers.antiWh = kh3DMarkersAntiWh[0] 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 = khFeatureEnabled('team') and khTeamEnabled[0] or false mainCfg.Team.host = khTeam.host mainCfg.Team.port = khTeamPort[0] mainCfg.Team.token = khTeam.token mainCfg.Team.manualServer = '' mainCfg.Team.mapEnabled = khFeatureEnabled('teamMap') and khTeamMapEnabled[0] and true or false mainCfg.Team.mapHold = khTeamMapHoldMode ~= nil and khTeamMapHoldMode[0] and true or false mainCfg.Team.mapTransparency = khClampInt(khTeamMapTransparency and khTeamMapTransparency[0], 0, 0, 80) mainCfg.Team.showNearby = khTeamShowNearby ~= nil and khTeamShowNearby[0] and true or false mainCfg.Team.mapKey = math.floor((tonumber(khTeamMapKey) or 0x47) + 0.5) mainCfg.Team.mapX = math.floor((tonumber(khTeamMapPos and khTeamMapPos.x) or 0) + 0.5) mainCfg.Team.mapY = math.floor((tonumber(khTeamMapPos and khTeamMapPos.y) or 0) + 0.5) mainCfg.Team.mapSize = math.floor((tonumber(khTeamMapSize) or 0) + 0.5) mainCfg.Spawns.enabled = khSpawnFeatureEnabled ~= nil and khSpawnFeatureEnabled[0] and true or false mainCfg.Spawns.mapEnabled = khSpawnMapEnabled ~= nil and khSpawnMapEnabled[0] and true or false mainCfg.Tyan.enabled = khFeatureEnabled('companion') and khTyanEnabled[0] or false 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() khStartMainMarksRemoteUpdate() 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 .. 'Точки перезагружаются.', -1, 'info', 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 function khSetMainTreasureMarker(index) if khIsNoTreasureServer() then khClearMainTreasureMarker() khClearMainTreasureBlips() khClear3DPickups() 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 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 mainCfg.LastZone.savedAt = saved and os.time() or 0 inicfg.save(mainCfg, config_file) end function khMarkZoneInactive() if type(removeGangZone) == 'function' then removeGangZone(610) end zoneActive = false mapUsed = false khSyncOwnTeamZone(false) khClearMainTreasureMarker() khSaveZoneState(false) khRefreshMainTreasureBlips(true) khTeamQueueZoneNow() end function khMarkZoneActive() if khIsViceCitySleepMode() then return false end zoneActive = true mapUsed = true khResetMainTreasureChecked() khSaveZoneState(true) khSyncOwnTeamZone(true,left,up,right,down,mainCfg.LastZone.savedAt) khRefreshMainTreasureBlips(true) khTeamQueueZoneNow() end function khExpireTreasureZones(showNotify) local expiredOwn=false if zoneActive then local record=mainCfg and mainCfg.LastZone or khTeamOwnZone if not khZoneIsFresh(record) then khMarkZoneInactive();expiredOwn=true end end local expiredTeam=false for token,member in pairs(khTeamMembers or {}) do local zone=type(member.zone)=='table' and member.zone or nil if zone and zone.active~=false and not khTeamZoneIsFresh(member,token) then zone.active=false;expiredTeam=true end end if expiredTeam and type(khTeamRefreshMapArtifacts)=='function' then khTeamRefreshMapArtifacts() end if expiredOwn and showNotify then nakhodkaNotify(sName..khZoneExpiredMessage,-1,'info',3) end return expiredOwn or expiredTeam end function khRestoreLastZone(notify) if not khIsScriptEnabled() then return false end if khZoneOpsEnabled == nil or not khZoneOpsEnabled[0] then return false end if khIsViceCitySleepMode() then return false end local lastZone = mainCfg and mainCfg.LastZone if not lastZone or not lastZone.saved then return false end if not khZoneIsFresh(lastZone) then lastZone.saved=false;lastZone.savedAt=0;inicfg.save(mainCfg,config_file);khSyncOwnTeamZone(false);return false end if type(addGangZone) ~= 'function' or type(removeGangZone) ~= 'function' then return false end if lastZone.left == nil or lastZone.up == nil or lastZone.right == nil or lastZone.down == nil then return false end left = lastZone.left up = lastZone.up right = lastZone.right down = lastZone.down removeGangZone(610) addGangZone(610, left, up, right, down, -2130706433) zoneActive = true mapUsed = true khSyncOwnTeamZone(true, left, up, right, down, lastZone.savedAt) khResetMainTreasureChecked() khRefreshMainTreasureBlips(true) if notify then nakhodkaNotify(sName .. '\xcf\xee\xf1\xeb\xe5\xe4\xed\xff\xff \xe7\xee\xed\xe0 \xe2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xeb\xe5\xed\xe0. \xd3\xe4\xe0\xeb\xe8\xf2\xfc: /zd', -1, 'info', 3) end return true end function khApplyZoneBlinkMode() if not khIsScriptEnabled() or khIsViceCitySleepMode() or not zoneActive then return false end if left == nil or up == nil or right == nil or down == nil then return false end if khDisableZoneBlink[0] then if kladZone ~= nil and tonumber(kladZone) ~= 610 then removeGangZone(kladZone) end removeGangZone(610) addGangZone(610, left, up, right, down, -2130706433) else removeGangZone(610) if kladZone ~= nil then addGangZone(kladZone, left, up, right, down, -16776961) end end return true end khApplyScriptEnabledState = function() khHudMoveMode = false khHudSaveLatch = false if khIsScriptEnabled() then khRestoreLastZone(false) khRefreshAdditionalBlips() khRefreshMainTreasureBlips(true) khSpawnRefreshMapBlips(true) khSpawnRefreshNearestBlip(true) nakhodkaNotify('Nakhodka включена.', -1, 'success', 2) else khClearActiveDopMarker() khRefreshAdditionalBlips() khClearMainTreasureBlips() khClear3DPickups() khClearMainTreasureMarker() khSpawnClearMapBlips() khSpawnClearNearestBlip() if type(removeGangZone) == 'function' then removeGangZone(610) end zoneActive = false mapUsed = false khSyncOwnTeamZone(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 khFeatureEnabled('companion') 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) while not sampIsLocalPlayerSpawned() do wait(0) end -- Сеть запускается после появления интерфейса. Ответы обрабатываются очередью -- главного цикла, поэтому Effil не может повторно войти в Lua-корутину игры. khRemoveRuntimeCleanupArtifacts() wait(0) if khZoneOpsEnabled ~= nil and not khZoneOpsEnabled[0] then khDisableZoneBlink[0] = false pcall(removeGangZone, 610) end 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 and khZoneIsFresh(mainCfg.LastZone) then zoneList:addZone(Zone:new(mainCfg.LastZone.left, mainCfg.LastZone.up, mainCfg.LastZone.right, mainCfg.LastZone.down)) elseif mainCfg.LastZone.saved then mainCfg.LastZone.saved=false mainCfg.LastZone.savedAt=0 inicfg.save(mainCfg,config_file) end khLoadAdditionalPoints() khRemoveExpiredAdditionalPoints(false) khRefreshAdditionalBlips() khLoadDropPrices() khLoadDropStats() khPruneDropPricesToKnownItems(true) khLoadMainTreasurePoints() khApiEnsureInstallId() if khIsScriptEnabled() and khRestoreLastZone(true) then khZoneNeedsRestore = false khZoneRestoreAt = 0 else khResetMainTreasureChecked() khRefreshMainTreasureBlips(true) end khSpawnRefreshMapBlips(true) khSpawnRefreshNearestBlip(true) zonedel_func = function() if not khCommandEnabled() then return end if khIsViceCitySleepMode() 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 khIsViceCitySleepMode() then return end if khZoneOpsEnabled ~= nil and not khZoneOpsEnabled[0] then nakhodkaNotify(sName .. 'Операции с зоной выключены.', -1, 'info') return end if zoneActive 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 khIsViceCitySleepMode() 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 khIsViceCitySleepMode() 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 khIsViceCitySleepMode() 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, nil, nil, 'points') 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 khDeletedGangZoneSnapshots = {} local saved = 0 for id, zone in pairs(khGangZoneSnapshots) do if tonumber(id) ~= 610 and type(zone) == 'table' then khDeletedGangZoneSnapshots[id] = { id = tonumber(zone.id) or tonumber(id), left = tonumber(zone.left), up = tonumber(zone.up), right = tonumber(zone.right), down = tonumber(zone.down), color = tonumber(zone.color) } saved = saved + 1 end end khGangZonesHidden = true for i = #gangZones, 1, -1 do local entry = gangZones[i] local id = type(entry) == 'table' and entry.id or entry if tonumber(id) ~= 610 then removeGangZone(id) end end -- Keep the legacy full ID sweep so /dgz behaves exactly as before. for id = 0, 1023 do if id ~= 610 then removeGangZone(id) end end nakhodkaNotify(sName .. '\xc7\xee\xed\xfb \xe3\xe5\xf2\xf2\xee \xe8 \xf1\xe5\xec\xe5\xe9 \xf3\xe4\xe0\xeb\xe5\xed\xfb. \xd1\xee\xf5\xf0\xe0\xed\xe5\xed\xee: ' .. tostring(saved) .. '. \xc2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xe8\xf2\xfc: /rgz', -1, 'success', 4) end restoregangzones = function(arg) if not khCommandEnabled() then return end local restored = 0 for id, zone in pairs(khDeletedGangZoneSnapshots) do local zoneId = tonumber(zone and zone.id) or tonumber(id) local l, u = tonumber(zone and zone.left), tonumber(zone and zone.up) local r, d = tonumber(zone and zone.right), tonumber(zone and zone.down) local color = tonumber(zone and zone.color) if zoneId ~= nil and zoneId ~= 610 and l ~= nil and u ~= nil and r ~= nil and d ~= nil and color ~= nil then addGangZone(zoneId, l, u, r, d, color) restored = restored + 1 end end if restored > 0 then khGangZonesHidden = false khDeletedGangZoneSnapshots = {} nakhodkaNotify(sName .. '\xc7\xee\xed\xfb \xe3\xe5\xf2\xf2\xee \xe8 \xf1\xe5\xec\xe5\xe9 \xe2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xeb\xe5\xed\xfb: ' .. tostring(restored) .. '.', -1, 'success', 4) else nakhodkaNotify(sName .. '\xcd\xe5\xf2 \xf1\xee\xf5\xf0\xe0\xed\xb8\xed\xed\xfb\xf5 \xe7\xee\xed \xe4\xeb\xff \xe2\xee\xf1\xf1\xf2\xe0\xed\xee\xe2\xeb\xe5\xed\xe8\xff. \xd1\xed\xe0\xf7\xe0\xeb\xe0 \xe2\xe2\xe5\xe4\xe8 /dgz.', -1, 'info', 4) end end sampRegisterChatCommand('delgangzones', delgangzones) sampRegisterChatCommand('dgz', delgangzones) sampRegisterChatCommand('restoregangzones', restoregangzones) sampRegisterChatCommand('rgz', restoregangzones) sampRegisterChatCommand('khunload', function() khUnloadNakhodka('command') end) sampRegisterChatCommand('khunl', function() khUnloadNakhodka('command') end) sampRegisterChatCommand('khteam', khTeamCmdOpen) sampRegisterChatCommand('khinvite', khTeamCmdInvite) sampRegisterChatCommand('khaccept', khTeamCmdAccept) sampRegisterChatCommand('khdeny', khTeamCmdDeny) sampRegisterChatCommand('khleave', khTeamCmdLeave) sampRegisterChatCommand('khkick', khTeamCmdKick) sampRegisterChatCommand('kht', khTeamCmdChat) sampRegisterChatCommand('khteamreconnect', khTeamCmdReconnect) sampRegisterChatCommand('sos', khTeamCmdSos) sampRegisterChatCommand('soscancel', khTeamCmdCancelSos) khStartTeamClient() sampRegisterChatCommand('nakhodka', toggleNakhodkaMenu) sampRegisterChatCommand('kh', toggleNakhodkaMenu) local khStartNickname = khGetMyNickname() or 'игрок' nakhodkaNotify('Nakhodka загружена. Приятного поиска кладов, ' .. khStartNickname, -1, 'success', 3) local khStartupAutoUpdateAt = os.clock() + 3.0 local khStartupAutoUpdateStarted = false khDropHudReady = true local wasSpawned = true while true do wait(0) local now = os.clock() local spawned = sampIsLocalPlayerSpawned() local previousSpawned = wasSpawned if spawned and previousSpawned == false then khApiResetMovementSession() end if wasSpawned and not spawned then khZoneNeedsRestore = true khZoneRestoreAt = now + 2 elseif spawned and khZoneNeedsRestore and now >= (tonumber(khZoneRestoreAt) or 0) then if khIsScriptEnabled() and mainCfg and mainCfg.LastZone and mainCfg.LastZone.saved then khRestoreLastZone(true) end khZoneNeedsRestore = false khZoneRestoreAt = 0 end wasSpawned = spawned local viceCitySleep = khIsViceCitySleepMode() if viceCitySleep then if not khViceCitySleepState then khViceCitySleepState = true removeGangZone(610) zoneActive = false mapUsed = false khSyncOwnTeamZone(false) khResetMainTreasureChecked() khClearMainTreasureBlips() khClearMainTreasureMarker() khClear3DPickups() local viceCityMessage = sName .. string.char(205, 224) .. ' Vice City ' .. string.char(234, 235, 224, 228, 251) .. ' ' .. string.char(237, 229, 228, 238, 241, 242, 243, 239, 237, 251, 46) .. ' ' .. string.char(208, 224, 225, 238, 242, 224, 254, 242) .. ' ' .. string.char(242, 238, 235, 252, 234, 238) .. ' ' .. string.char(234, 238, 236, 224, 237, 228, 237, 224, 255) .. ' ' .. string.char(241, 232, 241, 242, 229, 236, 224, 44) .. ' ' .. string.char(241, 239, 243, 242, 237, 232, 234) .. ' ' .. string.char(232) .. ' ' .. string.char(241, 242, 224, 242, 232, 241, 242, 232, 234, 224, 46) nakhodkaNotify(viceCityMessage, -1, 'info', 5) end else khViceCitySleepState = false end khRunDeferredJobs(4) khUpdateDigCursorLock(now) khApiTick(now) khPresenceTick(now) khRoadAutoClearArrivedWaypoint(now) if not khStartupAutoUpdateStarted and khAutoUpdateEnabled[0] and now >= khStartupAutoUpdateAt then khStartupAutoUpdateStarted = true khCheckForScriptUpdate(false) end if khIsScriptEnabled() and khFeatureEnabled('pointcheck') and not khIsViceCitySleepMode() 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) khExpireTreasureZones(true) end khRender3DWorldMarkers() if khFeatureEnabled('team') then khTeamUpdate(now) end khUnloadHandleHotkey() if khFeatureEnabled('teamMap') then khTeamMapHandleHotkey(now) khRenderTeamMapOverlay(now) end khSpawnUpdate(now, spawned, previousSpawned) if not khIsViceCitySleepMode() then khUpdateCefDropSession(now) khUpdatePendingDropSession(now) end -- 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.onSetMapIcon(iconId, position, iconType, color, style) if khHideServerIcons ~= nil and khHideServerIcons[0] then return false end end function sampev.onSetRaceCheckpoint(checkpointType, position, nextPosition, size) khRememberRaceCheckpoint(position) -- Some /findihouse replies are race checkpoints. if khSpawnScan ~= nil and khSpawnScan.waiting ~= nil then khSpawnRememberCheckpoint(position, size) end end function sampev.onSetCheckpoint(position, radius) khSpawnRememberCheckpoint(position, radius) end function sampev.onSendCommand(command) khPresenceHandleIdCommand(command) end function sampev.onShowDialog(id, style, title, button1, button2, text) if khSpawnHandleDialog(id, title, text) then return false end end function sampev.onSendDialogResponse(id, button, listboxId, input) khSpawnHandleDialogResponse(id, button, listboxId) end function sampev.onInitGame() khZoneNeedsRestore = true khZoneRestoreAt = os.clock() + 2 end function sampev.onServerMessage(color, message) if type(khPresenceHandleIdServerMessage)=='function' and khPresenceHandleIdServerMessage(message) then return false end local scriptEnabled = khIsScriptEnabled() khHandleDopServerMessage(message, not scriptEnabled) if not scriptEnabled then return end khSpawnHandleScanServerMessage(message) khHandlePreDigServerMessage(message) if khHandleCefDropServerMessage(message) then return end khHandleDropServerMessage(message) end function sampev.onCreateGangZone(zoneId, squareStart, squareEnd, color) if not khIsScriptEnabled() or khIsViceCitySleepMode() or khZoneOpsEnabled == nil or not khZoneOpsEnabled[0] then return end local normalizedId = tonumber(zoneId) or zoneId if khGangZoneLocalAdding[normalizedId] then return end if zoneId == 1023 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() khSaveZoneState(true) khSyncOwnTeamZone(true, left, up, right, down, mainCfg.LastZone.savedAt) 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) if khDisableZoneBlink[0] then lua_thread.create(function() wait(0) if zoneActive then pcall(removeGangZone, zoneId) end end) end else local id = tonumber(zoneId) or zoneId local snapshot = { id = tonumber(zoneId), left = tonumber(squareStart and squareStart.x), up = tonumber(squareStart and squareStart.y), right = tonumber(squareEnd and squareEnd.x), down = tonumber(squareEnd and squareEnd.y), color = tonumber(color) } khGangZoneSnapshots[id] = snapshot local known = false for _, oldId in ipairs(gangZones) do if tonumber(type(oldId) == 'table' and oldId.id or oldId) == tonumber(id) then known = true break end end if not known then table.insert(gangZones, id) end if khGangZonesHidden then khDeletedGangZoneSnapshots[id] = snapshot lua_thread.create(function() wait(0) if khGangZonesHidden then pcall(removeGangZone, id) end end) end end end function sampev.onGangZoneDestroy(zoneId1) if not khIsScriptEnabled() or khIsViceCitySleepMode() or khZoneOpsEnabled == nil or not khZoneOpsEnabled[0] then return end local normalizedId = tonumber(zoneId1) or zoneId1 if khGangZoneLocalRemoving[normalizedId] then return end if zoneId1 == kladZone then removeGangZone(610) addGangZone(610, left, up, right, down, -2130706433) zoneActive = true khSyncOwnTeamZone(true, left, up, right, down) khResetMainTreasureChecked() khRefreshMainTreasureBlips(true) if not khDisableZoneBlink[0] then 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) end 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 else khGangZoneSnapshots[normalizedId] = nil khDeletedGangZoneSnapshots[normalizedId] = nil for i = #gangZones, 1, -1 do local oldId = type(gangZones[i]) == 'table' and gangZones[i].id or gangZones[i] if tonumber(oldId) == tonumber(normalizedId) then table.remove(gangZones, i) end end end end function addGangZone(id, left, up, right, down, color) local normalizedId = tonumber(id) or id khGangZoneLocalAdding[normalizedId] = true 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) khGangZoneLocalAdding[normalizedId] = nil end function removeGangZone(id) local normalizedId = tonumber(id) or id khGangZoneLocalRemoving[normalizedId] = true local bs = raknetNewBitStream() raknetBitStreamWriteInt16(bs, id) raknetEmulRpcReceiveBitStream(120, bs) raknetDeleteBitStream(bs) khGangZoneLocalRemoving[normalizedId] = nil end 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(khStopDigCursorLock) 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