-- Golden Klad -- modules: markers3d | pointcheck | dopki | digCursor | license -- points: moonloader/resource/Golden_Klad/Golden_Klad_Coords.json (from API) script_name('Golden Klad') script_author('Golden Klad') script_version('1.0.4') local sampev = require 'lib.samp.events' local encoding = require 'encoding' encoding.default = 'CP1251' local u8 = encoding.UTF8 local imgui_ok, imgui = pcall(require, 'mimgui') local new = imgui_ok and imgui.new or nil ---------------------------------------------------------------------- -- paths ---------------------------------------------------------------------- local function workDir() if type(getWorkingDirectory) == 'function' then local ok, d = pcall(getWorkingDirectory) if ok and d and tostring(d) ~= '' then return tostring(d) end end return 'moonloader' end local function join(a, b) a, b = tostring(a or ''), tostring(b or ''):gsub('/', '\\') if a == '' then return b end return a .. '\\' .. b end local CONFIG_DIR = join(workDir(), 'config') local RESOURCE_DIR = join(join(workDir(), 'resource'), 'Golden_Klad') local COORDS_PATH = join(RESOURCE_DIR, 'Golden_Klad_Coords.json') -- legacy fallback (старый путь, если resource ещё пуст) local COORDS_LEGACY = join(workDir(), 'Golden_Klad_Coords.json') -- зашифрованный кэш точек (основное хранилище после загрузки с API) local CACHE_PATH = join(RESOURCE_DIR, 'gk.cashe') local SETTINGS_PATH = join(CONFIG_DIR, 'golden_klad_settings.ini') local DOP_PATH = join(CONFIG_DIR, 'golden_klad_dopklads.txt') local function readFile(path) local f = io.open(path, 'rb') if not f then return nil end local d = f:read('*a'); f:close(); return d end local function writeFile(path, data) local f = io.open(path, 'wb') if not f then return false end f:write(tostring(data or '')); f:close(); return true end local function ensureDir(path) if type(createDirectory) == 'function' and type(doesDirectoryExist) == 'function' then if not doesDirectoryExist(path) then pcall(createDirectory, path) end end end local function ensureConfigDir() ensureDir(CONFIG_DIR) ensureDir(join(workDir(), 'resource')) ensureDir(RESOURCE_DIR) end local function notify(msg) -- log only by default print('[Golden Klad] ' .. tostring(msg or '')) end -- SAMP chat: convert UTF-8 → CP1251 so Cyrillic displays correctly local function chatMsg(msg) msg = tostring(msg or '') print('[Golden Klad] ' .. msg) if type(sampAddChatMessage) ~= 'function' then return end local out = msg -- encoding.CP1251 converts UTF-8 string to CP1251 when available if type(encoding) == 'table' and type(encoding.CP1251) == 'function' then local ok, converted = pcall(encoding.CP1251, msg) if ok and converted then out = converted end elseif type(u8) == 'table' and type(u8.decode) == 'function' then local ok, converted = pcall(function() return u8:decode(msg) end) if ok and converted then out = converted end end pcall(sampAddChatMessage, '{E8B923}[Golden Klad]{FFFFFF} ' .. out, -1) end -- UI text: file is UTF-8; mimgui expects UTF-8. Guard with pcall in renderer. local function T(s) return tostring(s or '') end local ffi = nil do local ok, lib = pcall(require, 'ffi') if ok then ffi = lib end end local function ffi_string_safe(buf) if buf == nil then return '' end if type(buf) == 'string' then return buf end if ffi and type(ffi.string) == 'function' then local ok, s = pcall(ffi.string, buf) if ok and s then return s end end return tostring(buf) end local function setCharBuf(buf, str, maxLen) if not buf or not ffi then return end maxLen = maxLen or 128 str = tostring(str or '') if #str >= maxLen then str = str:sub(1, maxLen - 1) end pcall(function() ffi.fill(buf, maxLen) ffi.copy(buf, str) end) end ---------------------------------------------------------------------- -- settings ---------------------------------------------------------------------- local cfg = { scriptEnabled = true, markers3d = true, pointcheck = true, dopki = true, digCursor = true, onlyInZone = false, keepCheckedBlips = false, zoneOps = true, disableZoneBlink = true, showCircles = true, showArrows = true, blipSprite = 56, blipScale = 2, checkRadius = 3.0, maxBlips = 50, maxMarkers3d = 40, maxCircles = 40, circleRadius = 1.4, dopExpireHours = 3, zoneLeft = 0, zoneUp = 0, zoneRight = 0, zoneDown = 0, zoneSaved = false, zoneSavedAt = 0, -- license / API (XAMPP или хостинг) apiBase = 'https://goldenklad.shop', licenseKey = '', funpayKey = '', boostyLicenseKey = '', periodCode = '', licenseSource = '', licenseEmail = '', licenseToken = '', licenseExpires = '', licenseExpiresUnix = 0, coordsVersion = '0', scriptRemoteVersion = '0', autoUpdate = true, counterEnabled = true, showCounter = true, counterPosX = 1750, counterPosY = 250, } local function loadSettings() local raw = readFile(SETTINGS_PATH) if not raw then return end for line in raw:gmatch('[^\r\n]+') do local key, val = line:match('^([%w_]+)%s*=%s*(.*)$') if key and cfg[key] ~= nil and val then val = val:gsub('%s+$', '') local t = type(cfg[key]) if t == 'boolean' then cfg[key] = (val == 'true' or val == '1') elseif t == 'number' then local n = tonumber(val) if n ~= nil then cfg[key] = n end else cfg[key] = val end end end end local function saveSettings() ensureConfigDir() local lines = { '[GoldenKlad]' } local keys = {} for k in pairs(cfg) do keys[#keys + 1] = k end table.sort(keys) for _, k in ipairs(keys) do lines[#lines + 1] = k .. '=' .. tostring(cfg[k]) end writeFile(SETTINGS_PATH, table.concat(lines, '\n')) end ---------------------------------------------------------------------- -- data ---------------------------------------------------------------------- local mainPoints, checked, mainBlips, markers3d = {}, {}, {}, {} local dopPoints, nextDopId = {}, 1 local zone = nil ---------------------------------------------------------------------- -- Treasure counter (renderFont HUD — камера свободна) ---------------------------------------------------------------------- local KLAD_COUNT_PATH = join(CONFIG_DIR, 'golden_klad_count.txt') local kladCount = { total = 0, today = 0, week = 0, date = os.date('%Y-%m-%d'), weekKey = '' } local countDigPendingUntil = 0 local lastCountedDigAt = 0 local counterFont = nil -- ключ недели: понедельник текущей недели (YYYY-MM-DD) local function currentWeekKey() local t = os.date('*t') local wday = tonumber(t.wday) or 1 -- 1=вс .. 7=сб local daysFromMon = (wday + 5) % 7 -- пн=0 local monday = os.time({ year = t.year, month = t.month, day = t.day, hour = 0, min = 0, sec = 0, isdst = t.isdst, }) - daysFromMon * 86400 return os.date('%Y-%m-%d', monday) end local function saveKladCount() ensureConfigDir() return writeFile(KLAD_COUNT_PATH, string.format('%d|%s|%d|%s|%d', math.max(0, math.floor(tonumber(kladCount.total) or 0)), tostring(kladCount.date or os.date('%Y-%m-%d')), math.max(0, math.floor(tonumber(kladCount.today) or 0)), tostring(kladCount.weekKey or currentWeekKey()), math.max(0, math.floor(tonumber(kladCount.week) or 0)))) end local function loadKladCount() local raw = readFile(KLAD_COUNT_PATH) if raw then local total, dateText, today, weekKey, week = raw:match('^(%d+)|([^|]*)|(%d+)|([^|]*)|(%d+)') if total then kladCount.total = tonumber(total) or 0 kladCount.date = tostring(dateText or '') kladCount.today = tonumber(today) or 0 kladCount.weekKey = tostring(weekKey or '') kladCount.week = tonumber(week) or 0 else total, dateText, today = raw:match('^(%d+)|([^|]*)|(%d+)') if total then kladCount.total = tonumber(total) or 0 kladCount.date = tostring(dateText or '') kladCount.today = tonumber(today) or 0 kladCount.week = kladCount.today kladCount.weekKey = currentWeekKey() end end end local todayDate = os.date('%Y-%m-%d') if kladCount.date ~= todayDate then kladCount.date = todayDate kladCount.today = 0 end local wk = currentWeekKey() if tostring(kladCount.weekKey or '') ~= wk then kladCount.weekKey = wk kladCount.week = 0 end saveKladCount() end local function ensurePeriodStats() local todayDate = os.date('%Y-%m-%d') if kladCount.date ~= todayDate then kladCount.date = todayDate kladCount.today = 0 end local wk = currentWeekKey() if tostring(kladCount.weekKey or '') ~= wk then kladCount.weekKey = wk kladCount.week = 0 end end local function countCompletedKlad() if not cfg.counterEnabled then return end local now = os.clock() if now - lastCountedDigAt < 8.0 then return end lastCountedDigAt = now ensurePeriodStats() kladCount.total = (tonumber(kladCount.total) or 0) + 1 kladCount.today = (tonumber(kladCount.today) or 0) + 1 kladCount.week = (tonumber(kladCount.week) or 0) + 1 saveKladCount() end local function resetKladToday() ensurePeriodStats() kladCount.today = 0 saveKladCount() end local function resetKladWeek() ensurePeriodStats() kladCount.week = 0 kladCount.weekKey = currentWeekKey() saveKladCount() end local function resetKladAll() kladCount.total = 0 kladCount.today = 0 kladCount.week = 0 kladCount.date = os.date('%Y-%m-%d') kladCount.weekKey = currentWeekKey() saveKladCount() end local function toGameText(s) s = tostring(s or '') if type(encoding) == 'table' and type(encoding.CP1251) == 'function' then local ok, c = pcall(encoding.CP1251, s) if ok and c and c ~= '' then return c end end if type(u8) == 'table' and type(u8.decode) == 'function' then local ok, c = pcall(function() return u8:decode(s) end) if ok and c and c ~= '' then return c end end return s end local function ensureCounterFont() if counterFont then return counterFont end if type(renderCreateFont) == 'function' then local ok, f = pcall(renderCreateFont, 'Arial', 12, 5) if ok and f then counterFont = f end end return counterFont end --- HUD без imgui — не блокирует камеру -- зелёный = всё время, жёлтый = сегодня, красный = неделя local function renderCounterHud() if not cfg.counterEnabled or not cfg.showCounter or not cfg.scriptEnabled then return end ensurePeriodStats() local f = ensureCounterFont() if not f or type(renderFontDrawText) ~= 'function' then return end local x = tonumber(cfg.counterPosX) or 1750 local y = tonumber(cfg.counterPosY) or 250 local total = math.floor(tonumber(kladCount.total) or 0) local today = math.floor(tonumber(kladCount.today) or 0) local week = math.floor(tonumber(kladCount.week) or 0) local lines = { { toGameText('За всё время: ' .. tostring(total)), 0xFF2ECC66 }, -- green { toGameText('За сегодня: ' .. tostring(today)), 0xFFE8C83A }, -- yellow { toGameText('За неделю: ' .. tostring(week)), 0xFFE02020 }, -- red } local shadow = 0xC0000000 for i, row in ipairs(lines) do local yy = y + (i - 1) * 18 pcall(renderFontDrawText, f, row[1], x + 1, yy + 1, shadow) pcall(renderFontDrawText, f, row[1], x, yy, row[2]) end end local function parseCoordsJson(text) local loaded = {} text = tostring(text or ''):gsub('^\239\187\191', '') local num = '[-+]?%d+%.?%d*' for object in text:gmatch('{(.-)}') do local x = tonumber(object:match('["\']x["\']%s*:%s*(' .. num .. ')')) local y = tonumber(object:match('["\']y["\']%s*:%s*(' .. num .. ')')) local z = tonumber(object:match('["\']z["\']%s*:%s*(' .. num .. ')')) if x and y and z then loaded[#loaded + 1] = { x = x, y = y, z = z } end end if #loaded == 0 then for x, y, z in text:gmatch('%[%s*(' .. num .. ')%s*,%s*(' .. num .. ')%s*,%s*(' .. num .. ')%s*%]') do loaded[#loaded + 1] = { x = tonumber(x), y = tonumber(y), z = tonumber(z) } end end return loaded end ---------------------------------------------------------------------- -- Encrypted coords cache (gk.cashe) — obfuscation bound to license token -- Не секретность «навсегда», а: нет plain JSON + cache бесполезен без token ---------------------------------------------------------------------- local GK_CACHE_MAGIC = 'GK1\0' local GK_CACHE_SALT = 'GoldenKlad-cashe-v1' local function gkByteXor(a, b) -- Lua 5.1 compatible XOR for 0..255 local r, p = 0, 1 a, b = a % 256, b % 256 for _ = 1, 8 do local abit, bbit = a % 2, b % 2 if abit ~= bbit then r = r + p end a, b, p = math.floor(a / 2), math.floor(b / 2), p * 2 end return r end local function gkHashStr(s) s = tostring(s or '') local h = 2166136261 for i = 1, #s do h = (h * 16777619 + s:byte(i)) % 4294967296 end return string.format('%08X', h) end --- Ключ расшифровки: token + license key (+ salt). Без token cache не читается. local function gkCacheCryptoKey() local token = tostring(cfg.licenseToken or '') local key = tostring(cfg.licenseKey or '') if token == '' then return nil end return gkHashStr(token .. '|' .. key .. '|' .. GK_CACHE_SALT) .. GK_CACHE_SALT end local function gkCrypt(data, key) data = tostring(data or '') key = tostring(key or '') if key == '' or data == '' then return data end local state = {} for i = 0, 255 do state[i] = i end local j = 0 for i = 0, 255 do local kb = key:byte((i % #key) + 1) or 0 j = (j + state[i] + kb) % 256 state[i], state[j] = state[j], state[i] end local i = 0 j = 0 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 mask = state[(state[i] + state[j]) % 256] out[pos] = string.char(gkByteXor(data:byte(pos) or 0, mask)) end return table.concat(out) end local function gkChecksum(data) local h = 0 for i = 1, #data do h = (h + data:byte(i) + i * 17) % 1000000007 end return string.format('%08X', h) end --- Упаковка точек в компактный текст, затем XOR-обфускация local function gkPackPoints(points) local parts = {} for _, p in ipairs(points or {}) do local x, y, z = tonumber(p.x), tonumber(p.y), tonumber(p.z) if x and y and z then parts[#parts + 1] = string.format('%.3f,%.3f,%.3f', x, y, z) end end return table.concat(parts, ';') end local function gkUnpackPoints(payload) local loaded = {} payload = tostring(payload or '') for x, y, z in payload:gmatch('([%-%d%.]+),([%-%d%.]+),([%-%d%.]+)') do loaded[#loaded + 1] = { x = tonumber(x), y = tonumber(y), z = tonumber(z) } end return loaded end local function deleteCoordsPlainFiles() pcall(os.remove, COORDS_PATH) if COORDS_LEGACY then pcall(os.remove, COORDS_LEGACY) end end local function deleteCoordsCache() pcall(os.remove, CACHE_PATH) end local function writeCoordsCache(points) local key = gkCacheCryptoKey() if not key then return false, 'no_token' end ensureConfigDir() local payload = gkPackPoints(points) local sum = gkChecksum(payload) local body = sum .. '\n' .. payload local encrypted = gkCrypt(body, key) local blob = GK_CACHE_MAGIC .. encrypted if not writeFile(CACHE_PATH, blob) then return false, 'write_fail' end -- plain JSON больше не нужен deleteCoordsPlainFiles() return true, #points end local function readCoordsCache() local key = gkCacheCryptoKey() if not key then return nil, 'no_token' end local blob = readFile(CACHE_PATH) if not blob or blob == '' then return nil, 'missing' end if blob:sub(1, #GK_CACHE_MAGIC) ~= GK_CACHE_MAGIC then return nil, 'bad_magic' end local encrypted = blob:sub(#GK_CACHE_MAGIC + 1) local body = gkCrypt(encrypted, key) local sum, payload = body:match('^([0-9A-Fa-f]+)\n(.+)$') if not sum or not payload then return nil, 'bad_format' end if gkChecksum(payload) ~= sum:upper() and gkChecksum(payload) ~= sum then -- неверный token/ключ или файл повреждён return nil, 'bad_checksum' end local points = gkUnpackPoints(payload) if not points or #points == 0 then return nil, 'empty' end return points, nil end --- Активная подписка: ключ + токен + срок не истёк local function licenseIsActive() local key = tostring(cfg.licenseKey or '') local token = tostring(cfg.licenseToken or '') if key == '' or token == '' then return false end local u = tonumber(cfg.licenseExpiresUnix) if u and u > 0 then return u > os.time() end local exp = tostring(cfg.licenseExpires or '') if exp == '' then return false end local y, m, d, hh, mm, ss = exp:match('(%d%d%d%d)%-(%d%d)%-(%d%d)[ T](%d%d):(%d%d):(%d%d)') if not y then y, m, d, hh, mm = exp:match('(%d%d%d%d)%-(%d%d)%-(%d%d)[ T](%d%d):(%d%d)') ss = 0 end if y then local t = os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = tonumber(hh) or 0, min = tonumber(mm) or 0, sec = tonumber(ss) or 0, isdst = false, }) if t and t <= os.time() then return false end end return true end local function loadCoords() ensureConfigDir() -- без активной подписки точки не используем if not licenseIsActive() then mainPoints = {} checked = {} return 0, 'no_license' end -- 1) основной источник: зашифрованный gk.cashe local cached, cerr = readCoordsCache() if cached and #cached > 0 then mainPoints = cached checked = {} notify('coords cache: ' .. tostring(#cached)) return #mainPoints, nil end -- 2) миграция со старого plain JSON → cache local raw = readFile(COORDS_PATH) if (not raw or raw == '') and COORDS_LEGACY then raw = readFile(COORDS_LEGACY) end if raw and raw ~= '' then local pts = parseCoordsJson(raw) if pts and #pts > 0 then local ok = writeCoordsCache(pts) if ok then notify('coords migrated to gk.cashe: ' .. tostring(#pts)) end mainPoints = pts checked = {} return #mainPoints, nil end end mainPoints = {} checked = {} return 0, cerr or 'missing' end ---------------------------------------------------------------------- -- license API (activate + coords download + version check) ---------------------------------------------------------------------- local licenseBusy = false local lastUpdateCheck = 0 local uiLicenseBuf = nil -- filled after imgui init local function apiUrl(path) local base = tostring(cfg.apiBase or 'https://goldenklad.shop'):gsub('/+$', '') path = tostring(path or '') if path:sub(1, 1) ~= '/' then path = '/' .. path end return base .. path end function getPlayerNick() local n = '' if type(sampGetLocalPlayerNickname) == 'function' then local ok, v = pcall(sampGetLocalPlayerNickname) if ok and v and tostring(v) ~= '' then n = tostring(v) end end if n == '' and type(sampGetLocalPlayerId) == 'function' and type(sampGetPlayerNickname) == 'function' then local ok, id = pcall(sampGetLocalPlayerId) if ok and type(id) == 'number' and id >= 0 then local ok2, v = pcall(sampGetPlayerNickname, id) if ok2 and v and tostring(v) ~= '' then n = tostring(v) end end end n = tostring(n or ''):gsub('^%s+', ''):gsub('%s+$', '') if n == '' then return '' end -- не подставлять имя ПК / Windows-пользователя как ник if type(os.getenv) == 'function' then local pc = tostring(os.getenv('COMPUTERNAME') or '') local user = tostring(os.getenv('USERNAME') or '') if pc ~= '' and n:lower() == pc:lower() then return '' end if user ~= '' and n:lower() == user:lower() then return '' end end return n end -- тот же ключ, что fp_key в config.php сайта GK_FP_KEY = 'GoldenKladFpKey2026ChangeMe' function getFingerprintPayload() local nick = getPlayerNick() local pc, user = '', '' if type(os.getenv) == 'function' then pc = tostring(os.getenv('COMPUTERNAME') or '') user = tostring(os.getenv('USERNAME') or '') end -- формат: NICK|PC|USER (ник может быть пустым до входа на сервер) if nick ~= '' and pc ~= '' and nick:lower() == pc:lower() then nick = '' end if nick ~= '' and user ~= '' and nick:lower() == user:lower() then nick = '' end local plain = tostring(nick or '') .. '|' .. pc .. '|' .. user local key = tostring(GK_FP_KEY or 'GoldenKladFpKey2026ChangeMe') if key == '' then key = 'GoldenKladFpKey2026ChangeMe' end local out = {} for i = 1, #plain do local a = plain:byte(i) local b = key:byte(((i - 1) % #key) + 1) or 0 local x, p = 0, 1 local aa, bb = a, b for _ = 1, 8 do local ab, bd = aa % 2, bb % 2 if ab ~= bd then x = x + p end aa, bb, p = math.floor(aa / 2), math.floor(bb / 2), p * 2 end out[i] = string.char(x) end local bin = table.concat(out) local b64alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' local res = {} for i = 1, #bin, 3 do local ba, bb, bc = bin:byte(i, i + 2) bb = bb or 0 bc = bc or 0 local pad = 0 if not bin:byte(i + 1) then pad = 2 elseif not bin:byte(i + 2) then pad = 1 end local n = ba * 65536 + bb * 256 + bc local c1 = math.floor(n / 262144) % 64 local c2 = math.floor(n / 4096) % 64 local c3 = math.floor(n / 64) % 64 local c4 = n % 64 res[#res + 1] = b64alpha:sub(c1 + 1, c1 + 1) res[#res + 1] = b64alpha:sub(c2 + 1, c2 + 1) res[#res + 1] = (pad == 2) and '=' or b64alpha:sub(c3 + 1, c3 + 1) res[#res + 1] = (pad >= 1) and '=' or b64alpha:sub(c4 + 1, c4 + 1) end return table.concat(res), nick end local function getHwid() local parts = {} if type(getWorkingDirectory) == 'function' then parts[#parts + 1] = tostring(getWorkingDirectory() or '') end if type(sampGetLocalPlayerNickname) == 'function' then local ok, n = pcall(sampGetLocalPlayerNickname) if ok and n then parts[#parts + 1] = tostring(n) end end local envName = 'pc' if type(os.getenv) == 'function' then envName = os.getenv('COMPUTERNAME') or os.getenv('USERNAME') or 'pc' end parts[#parts + 1] = tostring(envName) local s = table.concat(parts, '|') -- simple hash (Lua 5.1 / LuaJIT compatible, no bitwise ~) local h = 2166136261 for i = 1, #s do h = (h * 16777619 + s:byte(i)) % 4294967296 end return string.format('%08X', h) end local function httpGet(url, timeoutSec) timeoutSec = timeoutSec or 12 -- MoonLoader built-in if type(downloadUrlToFile) == 'function' then local tmp = join(CONFIG_DIR, 'gk_http_tmp.txt') ensureConfigDir() local done, body, err = false, nil, nil pcall(downloadUrlToFile, url, tmp, function(id, status) -- status 0 = done on many builds; also accept path exists if status == 0 or status == true or status == 1 then done = true end end) local t0 = os.clock() while not done and os.clock() - t0 < timeoutSec do wait(50) if readFile(tmp) then -- file appeared local elapsed = os.clock() - t0 if elapsed > 0.3 then done = true end end end wait(100) body = readFile(tmp) pcall(os.remove, tmp) if body and body ~= '' then return true, body end return false, 'download_fail' end -- luasocket fallback local ok, http = pcall(require, 'socket.http') if ok and http and http.request then local ltn12_ok, ltn12 = pcall(require, 'ltn12') local chunks = {} local one, code = http.request{ url = url, sink = ltn12_ok and ltn12.sink.table(chunks) or nil, timeout = timeoutSec, } if ltn12_ok then body = table.concat(chunks) if code == 200 or (body and body ~= '') then return true, body end return false, 'http_' .. tostring(code) end if type(one) == 'string' and one ~= '' then return true, one end end return false, 'no_http' end local function httpPostJson(url, tbl) local payload = '{}' -- minimal JSON encode for our fields local parts = {} for k, v in pairs(tbl or {}) do parts[#parts + 1] = string.format('"%s":"%s"', tostring(k), tostring(v):gsub('\\', '\\\\'):gsub('"', '\\"')) end payload = '{' .. table.concat(parts, ',') .. '}' if type(downloadUrlToFile) == 'function' then -- many MoonLoader builds only GET; try GET-style activate with query local q = {} for k, v in pairs(tbl or {}) do q[#q + 1] = tostring(k) .. '=' .. tostring(v):gsub('([^%w%-_%.~])', function(c) return string.format('%%%02X', string.byte(c)) end) end local full = url .. (url:find('?', 1, true) and '&' or '?') .. table.concat(q, '&') return httpGet(full) end local ok, http = pcall(require, 'socket.http') if ok and http then local ltn12_ok, ltn12 = pcall(require, 'ltn12') local chunks = {} local _, code = http.request{ url = url, method = 'POST', headers = { ['Content-Type'] = 'application/json', ['Content-Length'] = tostring(#payload), }, source = ltn12_ok and ltn12.source.string(payload) or nil, sink = ltn12_ok and ltn12.sink.table(chunks) or nil, } local body = table.concat(chunks) if code == 200 or (body and body ~= '') then return true, body end return false, 'http_' .. tostring(code) end return false, 'no_http' end local function jsonGet(str, key) str = tostring(str or '') local s = str:match('"' .. key .. '"%s*:%s*"([^"]*)"') if s then return s end local n = str:match('"' .. key .. '"%s*:%s*([%-%d%.]+)') if n then return n end local b = str:match('"' .. key .. '"%s*:%s*(true|false)') if b then return b end return nil end local function applyCoordsBody(body) if not body or body == '' then return false, 'empty' end if body:find('"ok"%s*:%s*false') then return false, jsonGet(body, 'error') or 'denied' end if not licenseIsActive() then return false, 'no_license' end ensureConfigDir() local pts = parseCoordsJson(body) if not pts or #pts == 0 then return false, 'parse_fail' end -- сразу в зашифрованный cache, plain JSON не оставляем local ok, n = writeCoordsCache(pts) if not ok then return false, tostring(n) end mainPoints = pts checked = {} pcall(refreshBlips) pcall(refreshMarkers3d) notify('coords cached: ' .. tostring(#pts)) return true, #pts end local function revokeLicenseLocal(reason) notify('license revoked: ' .. tostring(reason or '')) cfg.licenseToken = '' cfg.licenseExpires = '' cfg.licenseExpiresUnix = 0 cfg.coordsVersion = '0' mainPoints = {} checked = {} saveSettings() -- cache бесполезен без token, удаляем вместе с plain json deleteCoordsCache() deleteCoordsPlainFiles() if type(clearMainBlips) == 'function' then pcall(clearMainBlips) end if type(clearMarkers3d) == 'function' then pcall(clearMarkers3d) end for i, id in pairs(mainBlips) do if id and type(removeBlip) == 'function' then pcall(removeBlip, id) end mainBlips[i] = nil end for i, h in ipairs(markers3d) do if h and type(removeUser3dMarker) == 'function' then pcall(removeUser3dMarker, h) end markers3d[i] = nil end chatMsg('Подписка отключена: ' .. tostring(reason or 'нет доступа')) end local function downloadCoordsFromServer() local key = tostring(cfg.licenseKey or '') local token = tostring(cfg.licenseToken or '') if key == '' and token == '' then return false, 'no_key' end local url = apiUrl('/api/coords.php') local q = {} if token ~= '' then q[#q + 1] = 'token=' .. token:gsub('([^%w%-_%.~])', function(c) return string.format('%%%02X', c:byte()) end) end if key ~= '' then q[#q + 1] = 'key=' .. key:gsub('([^%w%-_%.~])', function(c) return string.format('%%%02X', c:byte()) end) end q[#q + 1] = 'hwid=' .. getHwid() url = url .. '?' .. table.concat(q, '&') local ok, body = httpGet(url, 20) if not ok then return false, tostring(body) end if type(body) == 'string' and body:find('"ok"%s*:%s*false') then local err = jsonGet(body, 'error') or 'denied' if err == 'banned' or err == 'expired' or err == 'unauthorized' or err == 'invalid_key' then revokeLicenseLocal(jsonGet(body, 'message') or err) end return false, err end local applied, n = applyCoordsBody(body) if not applied then return false, tostring(n) end return true, n end local function formatExpiresDisplay(expires) expires = tostring(expires or '') -- только дата, без часов и минут: 13.11.2026 local y, m, d = expires:match('(%d%d%d%d)%-(%d%d)%-(%d%d)') if y then return string.format('%s.%s.%s', d, m, y) end return expires end local function parseExpiresToOsTime(expires) -- приоритет: unix с сервера (без сдвига часового пояса) local u = tonumber(cfg.licenseExpiresUnix) if u and u > 0 then return u end expires = tostring(expires or '') local y, m, d, hh, mm, ss = expires:match('(%d%d%d%d)%-(%d%d)%-(%d%d)[ T](%d%d):(%d%d):(%d%d)') if not y then y, m, d, hh, mm = expires:match('(%d%d%d%d)%-(%d%d)%-(%d%d)[ T](%d%d):(%d%d)') ss = 0 end if not y then return nil end return os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = tonumber(hh) or 0, min = tonumber(mm) or 0, sec = tonumber(ss) or 0, isdst = false, }) end local function formatTimeLeft(expires) local t = parseExpiresToOsTime(expires) if not t then return '' end local left = math.floor(t - os.time()) if left <= 0 then return 'истекла' end local days = math.floor(left / 86400) local hours = math.floor((left % 86400) / 3600) local mins = math.floor((left % 3600) / 60) if days > 0 then return string.format('%d дн. %d ч. %d мин.', days, hours, mins) end if hours > 0 then return string.format('%d ч. %d мин.', hours, mins) end return string.format('%d мин.', mins) end local function activateLicense(key) key = tostring(key or ''):gsub('%s+', '') if key == '' then return false, 'empty_key' end if licenseBusy then return false, 'busy' end licenseBusy = true local ok, body = httpPostJson(apiUrl('/api/activate.php'), { key = key, hwid = getHwid(), nick = getPlayerNick(), fp = select(1, getFingerprintPayload()), }) licenseBusy = false if not ok then return false, tostring(body) end if jsonGet(body, 'ok') == 'false' or body:find('"ok"%s*:%s*false') then local err = jsonGet(body, 'error') or '' local msg = jsonGet(body, 'message') or err or 'denied' if err == 'banned' or err == 'expired' or err == 'invalid_key' or err == 'hwid_mismatch' then cfg.licenseKey = key revokeLicenseLocal(msg) end return false, msg end cfg.licenseKey = key cfg.funpayKey = key cfg.licenseSource = 'funpay' cfg.licenseToken = jsonGet(body, 'token') or cfg.licenseToken local prevUnix = tonumber(cfg.licenseExpiresUnix) or 0 cfg.licenseExpires = jsonGet(body, 'expires_at') or cfg.licenseExpires or '' do local eu = tonumber(jsonGet(body, 'expires_unix')) if eu and eu > 0 then -- админ продлил срок на сайте if prevUnix > 0 and eu > prevUnix + 1800 then local addDays = math.floor((eu - prevUnix) / 86400 + 0.5) if addDays < 1 then addDays = 1 end chatMsg(string.format('Лицензия продлена на %d дн.', addDays)) end cfg.licenseExpiresUnix = eu else -- fallback: ровно plan 24ч от сейчас, если unix не пришёл local parsed = nil local exp = cfg.licenseExpires local y, m, d, hh, mm, ss = tostring(exp):match('(%d%d%d%d)%-(%d%d)%-(%d%d)[ T](%d%d):(%d%d):(%d%d)') if y then parsed = os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = tonumber(hh), min = tonumber(mm), sec = tonumber(ss) or 0, isdst = false }) end if parsed then cfg.licenseExpiresUnix = parsed else cfg.licenseExpiresUnix = os.time() + 86400 end end end local coordsVer = body:match('"coords"%s*:%s*"([%d%.]+)"') if not coordsVer then coordsVer = body:match('"versions"%s*:%s*{.-"coords"%s*:%s*"([^"]+)"') end if coordsVer then cfg.coordsVersion = coordsVer end local scriptVer = body:match('"versions"%s*:%s*{.-"script"%s*:%s*"([^"]+)"') if scriptVer then cfg.scriptRemoteVersion = scriptVer end saveSettings() local dok, dn = downloadCoordsFromServer() if dok then chatMsg('Ключ OK. Точек: ' .. tostring(dn)) return true, dn end -- coords denied may mean ban mid-flight local derr = tostring(dn or '') if derr == 'banned' or derr == 'expired' or derr == 'unauthorized' or derr == 'denied' then revokeLicenseLocal(derr) return false, derr end chatMsg('Ключ OK, coords: ' .. derr) return true, dn end --- Boosty: почта + код периода (первая активация или продление) local function redeemPeriodCode(email, code) email = tostring(email or ''):gsub('^%s+', ''):gsub('%s+$', ''):lower() code = tostring(code or ''):gsub('%s+', '') if email == '' then return false, 'empty_email' end if code == '' then return false, 'empty_code' end if licenseBusy then return false, 'busy' end licenseBusy = true local ok, body = httpPostJson(apiUrl('/api/redeem.php'), { email = email, code = code, hwid = getHwid(), nick = getPlayerNick(), fp = select(1, getFingerprintPayload()), }) licenseBusy = false if not ok then return false, tostring(body) end body = tostring(body or '') if jsonGet(body, 'ok') == 'false' or body:find('"ok"%s*:%s*false') then local err = jsonGet(body, 'error') or '' local msg = jsonGet(body, 'message') or err or 'denied' if err == 'banned' or err == 'hwid_mismatch' then revokeLicenseLocal(msg) end return false, msg end cfg.licenseEmail = jsonGet(body, 'email') or email cfg.periodCode = code local key = jsonGet(body, 'license_key') if key and key ~= '' then cfg.licenseKey = key cfg.boostyLicenseKey = key cfg.licenseSource = 'boosty' end cfg.licenseToken = jsonGet(body, 'token') or cfg.licenseToken local prevUnix = tonumber(cfg.licenseExpiresUnix) or 0 local exp = jsonGet(body, 'expires_at') if exp then cfg.licenseExpires = exp end local eu = tonumber(jsonGet(body, 'expires_unix')) local extDays = tonumber(jsonGet(body, 'extended_days')) or 0 if eu and eu > 0 then cfg.licenseExpiresUnix = eu end local isNew = body:find('"is_new"%s*:%s*true') ~= nil saveSettings() if isNew then chatMsg(extDays > 0 and string.format('Активировано на %d дн.', extDays) or 'Активировано.') else chatMsg(extDays > 0 and string.format('Лицензия продлена на %d дн.', extDays) or 'Лицензия продлена.') end local dok, dn = downloadCoordsFromServer() if dok then chatMsg('Точек: ' .. tostring(dn)) return true, dn end local derr = tostring(dn or '') if derr == 'banned' or derr == 'expired' or derr == 'unauthorized' or derr == 'denied' then revokeLicenseLocal(derr) return false, derr end return true, dn end local function responseIsDenied(body) body = tostring(body or '') if body == '' then return false, '', '' end local denied = body:find('"ok"%s*:%s*false') or body:find('"ok"%s*:%s*"false"') if not denied then return false, '', '' end local err = jsonGet(body, 'error') or '' local msg = jsonGet(body, 'message') or err or 'denied' return true, err, msg end --- Проверка ключа на сервере (бан / срок / HWID). Вызывать из lua_thread. local function validateLicenseOnline() local key = tostring(cfg.licenseKey or '') if key == '' then return false, 'no_key' end if licenseBusy then return false, 'busy' end licenseBusy = true local ok, body = httpPostJson(apiUrl('/api/activate.php'), { key = key, hwid = getHwid(), nick = getPlayerNick(), fp = select(1, getFingerprintPayload()), }) licenseBusy = false body = tostring(body or '') notify('validate http=' .. tostring(ok) .. ' body=' .. body:sub(1, 220)) if not ok or body == '' or body == 'download_fail' or body == 'no_http' then -- нет сети: не снимаем локальный доступ (офлайн), только лог notify('license check: no server response') return false, 'no_response' end local denied, err, msg = responseIsDenied(body) -- также ловим ok:false без кавычек вокруг false в разных форматах if not denied then local okVal = tostring(jsonGet(body, 'ok') or '') if okVal == 'false' or okVal == '0' then denied = true err = jsonGet(body, 'error') or 'denied' msg = jsonGet(body, 'message') or err end end if denied then -- полный сброс доступа: бан, срок, ключ, HWID revokeLicenseLocal(msg ~= '' and msg or err or 'нет доступа') return false, err ~= '' and err or msg end if not body:find('"ok"%s*:%s*true') and tostring(jsonGet(body, 'ok') or '') ~= 'true' then return false, 'bad_response' end local token = jsonGet(body, 'token') if token then cfg.licenseToken = token end local prevUnix = tonumber(cfg.licenseExpiresUnix) or 0 local exp = jsonGet(body, 'expires_at') if exp then cfg.licenseExpires = exp end local eu = tonumber(jsonGet(body, 'expires_unix')) if eu and eu > 0 then if prevUnix > 0 and eu > prevUnix + 1800 then local addDays = math.floor((eu - prevUnix) / 86400 + 0.5) if addDays < 1 then addDays = 1 end chatMsg(string.format('Лицензия продлена на %d дн.', addDays)) end cfg.licenseExpiresUnix = eu end saveSettings() return true, 'ok' end local function stripAccessVisuals() mainPoints = {} checked = {} if type(clearMainBlips) == 'function' then pcall(clearMainBlips) end if type(clearMarkers3d) == 'function' then pcall(clearMarkers3d) end if type(clearDopBlips) == 'function' then pcall(clearDopBlips) end end local pendingScriptUpdate = nil -- remote version string if newer available local updateFooterNotice = nil -- { kind='ok'|'avail'|'err', text=..., expiresAt=unix } local function localScriptVersion() if thisScript and type(thisScript) == 'function' then local ok, s = pcall(thisScript) if ok and s and s.version then return tostring(s.version) end end return '1.0.4' end local function versionIsNewer(remote, localv) local function parts(v) local t = {} for n in tostring(v or '0'):gmatch('%d+') do t[#t + 1] = tonumber(n) or 0 end if #t == 0 then t[1] = 0 end return t end local a, b = parts(remote), parts(localv) local n = math.max(#a, #b) for i = 1, n do local x, y = a[i] or 0, b[i] or 0 if x > y then return true end if x < y then return false end end return false end local function currentScriptPath() if thisScript and type(thisScript) == 'function' then local ok, s = pcall(thisScript) if ok and s and s.path and tostring(s.path) ~= '' then return tostring(s.path) end end return join(workDir(), 'Golden_Klad.lua') end --- Скачать Golden_Klad.lua с сайта и заменить текущий файл, затем reloadScripts local function downloadAndApplyScriptUpdate() if licenseBusy then return false, 'busy' end licenseBusy = true local url = apiUrl('/api/script.php?public=1') local ok, body = httpGet(url, 45) licenseBusy = false if not ok or not body or #tostring(body) < 200 then return false, 'download_fail' end body = tostring(body) -- не принять JSON-ошибку вместо lua if body:find('"ok"%s*:%s*false') or body:match('^%s*{') then return false, 'server_error' end if not (body:find('script_name') or body:find('Golden Klad') or body:find('script_version')) then return false, 'bad_file' end local path = currentScriptPath() local bak = path .. '.bak' local old = readFile(path) if old and old ~= '' then pcall(writeFile, bak, old) end if not writeFile(path, body) then if old then pcall(writeFile, path, old) end return false, 'write_fail' end pendingScriptUpdate = nil ensureConfigDir() -- после reload открыть меню и показать успех pcall(writeFile, join(CONFIG_DIR, 'gk_reopen.flag'), '1') local verMark = tostring(cfg.scriptRemoteVersion or '') if verMark == '' then verMark = body:match('script_version%([\'"]([%d%.]+)[\'"]%)') or 'new' end pcall(writeFile, join(CONFIG_DIR, 'gk_update_ok.flag'), verMark) chatMsg('Скрипт обновлён. Перезагрузка...') wait(400) if type(reloadScripts) == 'function' then pcall(reloadScripts) elseif thisScript and type(thisScript) == 'function' then local s = thisScript() if s and type(s.reload) == 'function' then pcall(function() s:reload() end) else chatMsg('Нажмите Ctrl+R чтобы применить обновление') end else chatMsg('Нажмите Ctrl+R чтобы применить обновление') end return true, 'ok' end local function checkUpdates() -- полный контроль: при каждом запуске / Ctrl+R сверяем ключ (FunPay и Boosty) local key = tostring(cfg.licenseKey or '') if key ~= '' then local vok, vmsg = validateLicenseOnline() if not vok then notify('license check fail: ' .. tostring(vmsg)) if vmsg ~= 'no_response' and vmsg ~= 'busy' then stripAccessVisuals() return end -- no_response: оставляем локальный кэш, но ниже coords с сервера не качаем без токена else notify('license check ok') end else -- нет ключа — точек с сервера быть не должно if not licenseIsActive() then stripAccessVisuals() deleteCoordsCache() deleteCoordsPlainFiles() end end if not licenseIsActive() then stripAccessVisuals() end -- версии: скрипт проверяем всегда; coords — только с активной подпиской local ok, body = httpGet(apiUrl('/api/version.php'), 10) if not ok or not body then return end local remoteCoords = body:match('"coords"%s*:%s*{.-"version"%s*:%s*"([^"]+)"') or body:match('"coords".-"version"%s*:%s*"([^"]+)"') local remoteScript = body:match('"script"%s*:%s*{.-"version"%s*:%s*"([^"]+)"') or body:match('"script".-"version"%s*:%s*"([^"]+)"') if remoteScript then cfg.scriptRemoteVersion = tostring(remoteScript) saveSettings() local localVer = localScriptVersion() if versionIsNewer(remoteScript, localVer) then pendingScriptUpdate = tostring(remoteScript) notify('script update available: ' .. tostring(remoteScript)) chatMsg(string.format('Доступна версия %s (сейчас %s). Меню → Активация → Обновить скрипт.', tostring(remoteScript), localVer)) else pendingScriptUpdate = nil end end if not cfg.autoUpdate then return end if not licenseIsActive() then return end if remoteCoords and tostring(remoteCoords) ~= tostring(cfg.coordsVersion or '0') then if tostring(cfg.licenseKey or '') ~= '' and tostring(cfg.licenseToken or '') ~= '' then notify('coords update ' .. tostring(cfg.coordsVersion) .. ' -> ' .. tostring(remoteCoords)) local dok, dn = downloadCoordsFromServer() if dok then cfg.coordsVersion = tostring(remoteCoords) saveSettings() chatMsg('База точек обновлена: ' .. tostring(dn)) elseif tostring(dn) == 'banned' or tostring(dn) == 'expired' then revokeLicenseLocal(dn) end end end end local zoneActive = false local zoneVisualId = 610 local kladZoneServerId = 1023 local gangZoneLocalAdding = {} local gangZoneLocalRemoving = {} local zoneNeedsRestore = false local zoneRestoreAt = 0 local function pointInZone(x, y) if not zone or not zoneActive then return false end local l = math.min(tonumber(zone.left) or 0, tonumber(zone.right) or 0) local r = math.max(tonumber(zone.left) or 0, tonumber(zone.right) or 0) local d = math.min(tonumber(zone.down) or 0, tonumber(zone.up) or 0) local u = math.max(tonumber(zone.down) or 0, tonumber(zone.up) or 0) x, y = tonumber(x) or 0, tonumber(y) or 0 return x >= l and x <= r and y >= d and y <= u end local function addGangZone(id, left, up, right, down, color) if type(raknetNewBitStream) ~= 'function' then return end local nid = tonumber(id) or id gangZoneLocalAdding[nid] = 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) gangZoneLocalAdding[nid] = nil end local function removeGangZone(id) if type(raknetNewBitStream) ~= 'function' then return end local nid = tonumber(id) or id gangZoneLocalRemoving[nid] = true local bs = raknetNewBitStream() raknetBitStreamWriteInt16(bs, id) raknetEmulRpcReceiveBitStream(120, bs) raknetDeleteBitStream(bs) gangZoneLocalRemoving[nid] = nil end local function drawSavedZoneVisual() if not zone or not zoneActive then return end pcall(removeGangZone, zoneVisualId) -- stable non-blinking fill (same color as original) pcall(addGangZone, zoneVisualId, zone.left, zone.up, zone.right, zone.down, -2130706433) end local function saveZoneToCfg() if zone and zoneActive then cfg.zoneLeft = zone.left cfg.zoneUp = zone.up cfg.zoneRight = zone.right cfg.zoneDown = zone.down cfg.zoneSaved = true cfg.zoneSavedAt = os.time() else cfg.zoneSaved = false cfg.zoneSavedAt = 0 end saveSettings() end local function setKladZone(left, up, right, down) zone = { left = tonumber(left), up = tonumber(up), right = tonumber(right), down = tonumber(down), } zoneActive = zone.left ~= nil and zone.up ~= nil and zone.right ~= nil and zone.down ~= nil if zoneActive then drawSavedZoneVisual() saveZoneToCfg() end end local function clearKladZone(silent) pcall(removeGangZone, zoneVisualId) zone = nil zoneActive = false cfg.zoneSaved = false cfg.zoneSavedAt = 0 saveSettings() if not silent then notify(T('Зона клада сброшена')) end end local function restoreZoneFromCfg() if not cfg.zoneSaved then return false end local l, u, r, d = tonumber(cfg.zoneLeft), tonumber(cfg.zoneUp), tonumber(cfg.zoneRight), tonumber(cfg.zoneDown) if not l or not u or not r or not d then return false end zone = { left = l, up = u, right = r, down = d } zoneActive = true drawSavedZoneVisual() notify(string.format('zone restored %.0f,%.0f — %.0f,%.0f', l, u, r, d)) return true end ---------------------------------------------------------------------- -- player ---------------------------------------------------------------------- local function getPed() if PLAYER_PED and PLAYER_PED ~= 0 then return PLAYER_PED end if type(getPlayerPed) == 'function' then local ok, ped = pcall(getPlayerPed, -1) if ok and ped and ped ~= 0 then return ped end end return nil end local function getPos() local ped = getPed() if not ped or type(getCharCoordinates) ~= 'function' then return nil end local ok, x, y, z = pcall(getCharCoordinates, ped) if ok and x then return x, y, z or 0 end return nil end local function isSpawned() if type(sampIsLocalPlayerSpawned) == 'function' then local ok, v = pcall(sampIsLocalPlayerSpawned) if ok then return v and true or false end end return getPos() ~= nil end ---------------------------------------------------------------------- -- blips ---------------------------------------------------------------------- local function addSpriteBlip(x, y, z, icon) icon = math.floor(tonumber(icon) or 56) x, y, z = tonumber(x), tonumber(y), tonumber(z) or 0 if not x or not y then return nil end if type(addSpriteBlipForCoord) == 'function' then local ok, b = pcall(addSpriteBlipForCoord, x, y, z, icon) if ok and b then return b end end if type(addShortRangeSpriteBlipForCoord) == 'function' then local ok, b = pcall(addShortRangeSpriteBlipForCoord, x, y, z, icon) if ok and b then return b end end if type(addBlipForCoord) == 'function' then local ok, b = pcall(addBlipForCoord, x, y, z) if ok and b then if type(changeBlipSprite) == 'function' then pcall(changeBlipSprite, b, icon) end return b end end return nil end local function safeRemoveBlip(blip) if blip and type(removeBlip) == 'function' then pcall(removeBlip, blip) end end local function styleBlip(blip, color) if not blip then return end if type(changeBlipScale) == 'function' then pcall(changeBlipScale, blip, tonumber(cfg.blipScale) or 2) end if type(changeBlipColour) == 'function' and color then pcall(changeBlipColour, blip, color) end if type(setBlipAsShortRange) == 'function' then pcall(setBlipAsShortRange, blip, false) end if type(changeBlipDisplay) == 'function' then pcall(changeBlipDisplay, blip, 3) end end local function clearMainBlips() for i, id in pairs(mainBlips) do safeRemoveBlip(id); mainBlips[i] = nil end end local function refreshBlips() clearMainBlips() if not licenseIsActive() then mainPoints = {} return 0 end if not cfg.scriptEnabled or not cfg.pointcheck or not isSpawned() then return 0 end if cfg.onlyInZone and not zoneActive then return 0 end local px, py = getPos() if not px then return 0 end local scored = {} for i, p in ipairs(mainPoints) do local allow = (not checked[i]) or cfg.keepCheckedBlips if allow and ((not cfg.onlyInZone) or pointInZone(p.x, p.y)) then local dx, dy = p.x - px, p.y - py scored[#scored + 1] = { i = i, dist2 = dx * dx + dy * dy, p = p, isChecked = checked[i] and true or false } end end table.sort(scored, function(a, b) return a.dist2 < b.dist2 end) local limit = math.max(1, math.floor(tonumber(cfg.maxBlips) or 50)) local n, icon = 0, math.floor(tonumber(cfg.blipSprite) or 56) for k = 1, math.min(limit, #scored) do local e = scored[k] local blip = addSpriteBlip(e.p.x, e.p.y, e.p.z, icon) if blip then -- green unchecked, red checked (when kept on map) styleBlip(blip, e.isChecked and 0xFFE02020 or 0xFF22CC55) mainBlips[e.i] = blip n = n + 1 end end return n end local function pointcheckTick() if not cfg.scriptEnabled or not cfg.pointcheck then return end local px, py, pz = getPos() if not px then return end local r2 = (tonumber(cfg.checkRadius) or 2.5) ^ 2 local changed = false for i, p in ipairs(mainPoints) do if not checked[i] then local dx, dy, dz = p.x - px, p.y - py, (p.z or 0) - (pz or 0) if dx * dx + dy * dy + dz * dz <= r2 then checked[i] = true if not cfg.keepCheckedBlips then safeRemoveBlip(mainBlips[i]); mainBlips[i] = nil elseif mainBlips[i] then -- recolor existing blip to red styleBlip(mainBlips[i], 0xFFE02020) end changed = true end end end if changed then refreshBlips() end end ---------------------------------------------------------------------- -- 3D arrows (user markers) ---------------------------------------------------------------------- local function clearMarkers3d() for _, h in ipairs(markers3d) do if type(h) == 'number' and type(removeUser3dMarker) == 'function' then pcall(removeUser3dMarker, h) end end markers3d = {} end local function create3dArrow(x, y, z) if type(createUser3dMarker) ~= 'function' then return nil end for _, typ in ipairs({ 4, 1, 2, 5, 0 }) do local ok, m = pcall(createUser3dMarker, x, y, z + 1.5, typ) if ok and type(m) == 'number' and m >= 0 then return m end end return nil end local function refreshMarkers3d() clearMarkers3d() if not licenseIsActive() then return 0 end if not cfg.scriptEnabled or not cfg.markers3d or not cfg.showArrows or not isSpawned() then return 0 end local px, py = getPos() if not px then return 0 end local scored = {} for i, p in ipairs(mainPoints) do if not checked[i] and ((not cfg.onlyInZone) or pointInZone(p.x, p.y)) then local dx, dy = p.x - px, p.y - py scored[#scored + 1] = { dist2 = dx * dx + dy * dy, p = p } end end for _, d in ipairs(dopPoints) do if d.showOnMap then local dx, dy = d.x - px, d.y - py scored[#scored + 1] = { dist2 = dx * dx + dy * dy, p = d } end end table.sort(scored, function(a, b) return a.dist2 < b.dist2 end) local limit = math.max(1, math.floor(tonumber(cfg.maxMarkers3d) or 40)) local n = 0 for k = 1, math.min(limit, #scored) do local h = create3dArrow(scored[k].p.x, scored[k].p.y, scored[k].p.z or 0) if h then markers3d[#markers3d + 1] = h; n = n + 1 end end return n end ---------------------------------------------------------------------- -- 3D ground circles (original-style, every frame) ---------------------------------------------------------------------- local function argb(a, r, g, b) a = math.max(0, math.min(255, math.floor(a + 0.5))) r = math.max(0, math.min(255, math.floor(r + 0.5))) g = math.max(0, math.min(255, math.floor(g + 0.5))) b = math.max(0, math.min(255, math.floor(b + 0.5))) return a * 16777216 + r * 65536 + g * 256 + b end local function worldOnScreen(x, y, z) if type(isPointOnScreen) ~= 'function' then return true end local ok, v = pcall(isPointOnScreen, x, y, z, 0.35) return (not ok) or (v ~= false) end local function worldToScreen(x, y, z) if type(convert3DCoordsToScreen) ~= 'function' then return nil, nil end if not worldOnScreen(x, y, z) then return nil, nil end local ok, sx, sy = pcall(convert3DCoordsToScreen, x, y, z) if not ok or not sx or not sy then return nil, nil end if sx ~= sx or sy ~= sy then return nil, nil end -- NaN return sx, sy end local function drawScreenLine(x1, y1, x2, y2, color, width) if type(renderDrawLine) ~= 'function' then return end pcall(renderDrawLine, x1, y1, x2, y2, width or 2, color) end local function drawWorldCircle(x, y, z, radius, color, width) radius = tonumber(radius) or 1.4 if radius < 0.2 then radius = 0.2 end if radius > 6 then radius = 6 end if not worldOnScreen(x, y, z) then return end local segments = 16 local prevX, prevY = nil, nil for i = 0, segments do local angle = (math.pi * 2) * (i / segments) local sx, sy = worldToScreen(x + math.cos(angle) * radius, y + math.sin(angle) * radius, z) if sx and sy then if prevX and math.abs(sx - prevX) < 200 and math.abs(sy - prevY) < 200 then drawScreenLine(prevX, prevY, sx, sy, color, width or 2) end prevX, prevY = sx, sy else prevX, prevY = nil, nil end end end local circleColorUnchecked = argb(220, 45, 220, 90) -- green: not checked local circleColorChecked = argb(220, 230, 55, 55) -- red: checked local circleColorDop = argb(220, 255, 180, 40) -- orange: dop local function renderGroundCircles() if not cfg.scriptEnabled or not cfg.markers3d or not cfg.showCircles then return end if not isSpawned() then return end local px, py = getPos() if not px then return end local scored = {} -- main points only with active license if licenseIsActive() then for i, p in ipairs(mainPoints) do if (not cfg.onlyInZone) or pointInZone(p.x, p.y) then local dx, dy = p.x - px, p.y - py local dist2 = dx * dx + dy * dy if dist2 < (90 * 90) then scored[#scored + 1] = { dist2 = dist2, p = p, kind = checked[i] and 'checked' or 'unchecked', } end end end end -- dop points (orange) if cfg.dopki then for _, d in ipairs(dopPoints) do if d.showOnMap then local dx, dy = d.x - px, d.y - py local dist2 = dx * dx + dy * dy if dist2 < (90 * 90) then scored[#scored + 1] = { dist2 = dist2, p = d, kind = 'dop' } end end end end table.sort(scored, function(a, b) return a.dist2 < b.dist2 end) local limit = math.max(1, math.floor(tonumber(cfg.maxCircles) or 40)) local rad = tonumber(cfg.circleRadius) or 1.4 for k = 1, math.min(limit, #scored) do local e = scored[k] local col = circleColorUnchecked if e.kind == 'checked' then col = circleColorChecked elseif e.kind == 'dop' then col = circleColorDop end drawWorldCircle(e.p.x, e.p.y, (e.p.z or 0) + 0.05, rad, col, 2) end end ---------------------------------------------------------------------- -- Доп. Клады (algorithm from original, no Nakhodka branding) ---------------------------------------------------------------------- local dopExpectUntil = nil local lastRaceCheckpoint = nil local dopCheckpointBeforeDig = nil local dopDigLocation = nil local dopDigCapturedAt = 0 local dopCefDetectedAt = 0 local function copyPos(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 local function dopDist2(a, b) if not a or not b then return nil end local dx = (tonumber(a.x) or 0) - (tonumber(b.x) or 0) local dy = (tonumber(a.y) or 0) - (tonumber(b.y) or 0) local dz = (tonumber(a.z) or 0) - (tonumber(b.z) or 0) return dx * dx + dy * dy + dz * dz end local function isUniqueDop(x, y, z) local p = { x = x, y = y, z = z } for _, q in ipairs(dopPoints) do local d2 = dopDist2(p, q) if d2 and d2 < 4.0 then return false end -- 2m end return true end local function clearDopBlips() for _, d in ipairs(dopPoints) do safeRemoveBlip(d.blip); d.blip = nil end end local function saveDopki() ensureConfigDir() local lines = {} for _, d in ipairs(dopPoints) do lines[#lines + 1] = string.format('%d|%.4f|%.4f|%.4f|%d|%d|%d', d.id, d.x, d.y, d.z, tonumber(d.time) or os.time(), tonumber(d.icon) or 41, d.showOnMap and 1 or 0) end writeFile(DOP_PATH, table.concat(lines, '\n')) end local function loadDopki() clearDopBlips(); dopPoints = {} local raw = readFile(DOP_PATH) if not raw then return 0 end local n = 0 for line in raw:gmatch('[^\r\n]+') do local id, x, y, z, tm, icon, show = line:match('^(%d+)|([%-%d%.]+)|([%-%d%.]+)|([%-%d%.]+)|(%d+)|(%d+)|(%d+)$') if not id then id, x, y, z, tm, icon = line:match('^(%d+)|([%-%d%.]+)|([%-%d%.]+)|([%-%d%.]+)|(%d+)|(%d+)$') show = '0' end if not id then id, x, y, z = line:match('^(%d+)|([%-%d%.]+)|([%-%d%.]+)|([%-%d%.]+)$') tm, icon, show = tostring(os.time()), '41', '0' end if id then id = tonumber(id) dopPoints[#dopPoints + 1] = { id = id, x = tonumber(x), y = tonumber(y), z = tonumber(z), time = tonumber(tm) or os.time(), icon = tonumber(icon) or 41, showOnMap = (tonumber(show) or 0) == 1, } if id >= nextDopId then nextDopId = id + 1 end n = n + 1 end end return n end local function removeExpiredDops(silent) local hours = tonumber(cfg.dopExpireHours) or 3 local maxAge = hours * 3600 local now = os.time() local removed = 0 for i = #dopPoints, 1, -1 do if now - (tonumber(dopPoints[i].time) or now) > maxAge then safeRemoveBlip(dopPoints[i].blip) table.remove(dopPoints, i) removed = removed + 1 end end if removed > 0 then saveDopki() if not silent then notify('expired dop: ' .. tostring(removed)) end end return removed end local function refreshDopBlips() clearDopBlips() if not cfg.scriptEnabled or not cfg.dopki or not isSpawned() then return end removeExpiredDops(true) -- blips only for points the player chose to find (showOnMap) for _, d in ipairs(dopPoints) do if d.showOnMap then local blip = addSpriteBlip(d.x, d.y, d.z, d.icon or 41) if blip then styleBlip(blip, 0xFFFFAA00); d.blip = blip end else d.blip = nil end end end local dopNavCheckpoint = nil local dopNavPointId = nil local function clearDopNavigator() if dopNavCheckpoint and type(deleteCheckpoint) == 'function' then pcall(deleteCheckpoint, dopNavCheckpoint) end dopNavCheckpoint = nil dopNavPointId = nil if type(removeWaypoint) == 'function' then pcall(removeWaypoint) end end local function setDopNavigator(point) clearDopNavigator() if not point then return end if type(placeWaypoint) == 'function' then pcall(placeWaypoint, point.x, point.y, point.z or 0) end if type(createCheckpoint) == 'function' then local ok, h = pcall(createCheckpoint, 1, point.x, point.y, point.z or 0, 0.0, 0.0, 0.0, 2.5) if ok and h then dopNavCheckpoint = h dopNavPointId = point.id end end end local function setDopOnMap(id, enabled) for _, d in ipairs(dopPoints) do if d.id == id then d.showOnMap = enabled and true or false if not d.showOnMap then safeRemoveBlip(d.blip); d.blip = nil if dopNavPointId == id then clearDopNavigator() end else -- blip + GPS waypoint + checkpoint (as original) setDopNavigator(d) end refreshDopBlips() saveDopki() return true end end return false end local function addDopPoint(x, y, z, icon) x, y, z = tonumber(x), tonumber(y), tonumber(z) if not x or not y or not z then return false end if not isUniqueDop(x, y, z) then notify('dop already exists nearby') return false end local id = nextDopId nextDopId = nextDopId + 1 dopPoints[#dopPoints + 1] = { id = id, x = x, y = y, z = z, time = os.time(), icon = tonumber(icon) or 41, showOnMap = false, -- record only, no map mark until "Найти" } saveDopki() -- do not place map blip automatically notify(string.format('dop + #%d', id)) return true end local function addDopHere() local x, y, z = getPos() if not x then return false end return addDopPoint(x, y, z, 41) end local function removeDop(id) for i = #dopPoints, 1, -1 do if dopPoints[i].id == id then safeRemoveBlip(dopPoints[i].blip) if dopNavPointId == id then clearDopNavigator() end table.remove(dopPoints, i) saveDopki() return true end end return false end local function removeDopNear(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 bestI, bestD2 = nil, 16.0 -- 4m for i, point in ipairs(dopPoints) 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 d2 = dx * dx + dy * dy + dz * dz if d2 <= bestD2 then bestI, bestD2 = i, d2 end end if not bestI then return false end safeRemoveBlip(dopPoints[bestI].blip) table.remove(dopPoints, bestI) saveDopki() refreshDopBlips() if not silent then notify('dop removed') end return true end local function captureDopDigContext() local x, y, z = getPos() local now = os.clock() if x then dopDigLocation = { x = x, y = y, z = z, t = now } else dopDigLocation = nil end dopDigCapturedAt = now dopCheckpointBeforeDig = lastRaceCheckpoint and copyPos(lastRaceCheckpoint) or nil return true end local function clearDopExpectation() dopExpectUntil = nil end local function startExpectDopCheckpoint() dopExpectUntil = os.clock() + 2.5 end local function checkpointDiffersFromBefore(checkpoint) if dopCheckpointBeforeDig == nil then return true end local d2 = dopDist2(checkpoint, dopCheckpointBeforeDig) return d2 ~= nil and d2 > 0.25 end local function acceptDopCheckpoint(checkpoint) checkpoint = copyPos(checkpoint) if checkpoint == nil or not checkpointDiffersFromBefore(checkpoint) then return false end local ok = addDopPoint(checkpoint.x, checkpoint.y, checkpoint.z, 41) dopCefDetectedAt = os.clock() clearDopExpectation() return ok end local function rememberRaceCheckpoint(position) local checkpoint = copyPos(position) if checkpoint == nil then return end checkpoint.t = os.clock() lastRaceCheckpoint = checkpoint if cfg.scriptEnabled and cfg.dopki and dopExpectUntil and checkpoint.t <= dopExpectUntil then acceptDopCheckpoint(checkpoint) end end function sampev.onSetRaceCheckpoint(checkpointType, position, nextPosition, size) rememberRaceCheckpoint(position) end local function getMyNickname() if type(sampGetPlayerNickname) ~= 'function' then if type(sampGetLocalPlayerNickname) == 'function' then local ok, n = pcall(sampGetLocalPlayerNickname) if ok and n then return tostring(n) end end return nil end local id = nil if type(sampGetPlayerIdByCharHandle) == 'function' and PLAYER_PED then local ok, a, b = pcall(sampGetPlayerIdByCharHandle, PLAYER_PED) -- returns result, id or id if ok then if type(a) == 'number' then id = a elseif type(b) == 'number' then id = b end end end if type(id) ~= 'number' and type(select) == 'function' then local ok, res, pid = pcall(sampGetPlayerIdByCharHandle, PLAYER_PED) if ok and type(pid) == 'number' then id = pid elseif ok and type(res) == 'number' then id = res end end if type(id) == 'number' then local ok, nick = pcall(sampGetPlayerNickname, id) if ok and nick then return tostring(nick) end end return nil end local function normalizeName(s) s = tostring(s or ''):gsub('%s+', ' '):gsub('^%s+', ''):gsub('%s+$', '') return s:lower() end -- CP1251 fragments used by Arizona treasure messages local CP_TAG_KLADY = string.char(0x5B, 0xCA, 0xEB, 0xE0, 0xE4, 0xFB, 0x5D) -- [Клады] local CP_KLADOISK = string.char(0xCA, 0xEB, 0xE0, 0xE4, 0xEE, 0xE8, 0xF1, 0xEA, 0xE0, 0xF2, 0xE5, 0xEB, 0xFC) -- Кладоискатель local CP_SECRET_DOP = string.char( 0xEF, 0xEE, 0xEF, 0xFB, 0xF2, 0xE0, 0xEB, 0x20, 0xF3, 0xE4, 0xE0, 0xF7, 0xF3, 0x20, 0xE8, 0x20, 0xEF, 0xEE, 0xEB, 0xF3, 0xF7, 0xE8, 0xEB, 0x20, 0xF1, 0xE5, 0xEA, 0xF0, 0xE5, 0xF2, 0xED, 0xFB, 0xE9, 0x20, 0xE4, 0xEE, 0xEF, 0xEE, 0xEB, 0xED, 0xE8, 0xF2, 0xE5, 0xEB, 0xFC, 0xED, 0xFB, 0xE9, 0x20, 0xEA, 0xEB, 0xE0, 0xE4 ) -- попытался удачу и получил секретный дополнительный клад local CP_NACHAL = string.char(0xED, 0xE0, 0xF7, 0xE0, 0xEB) -- начал local CP_KOPAT = string.char(0xEA, 0xEE, 0xEF, 0xE0, 0xF2) -- копат local CP_SUCCESS_DIG = string.char(0xF3, 0xF1, 0xEF, 0xE5, 0xF8, 0xED, 0xEE, 0x20, 0xE4, 0xEE, 0xF1, 0xF2, 0xE0, 0xEB) -- успешно достал local function isOwnDopMessage(message) message = tostring(message or '') if not message:find(CP_TAG_KLADY, 1, true) or not message:find(CP_KLADOISK, 1, true) or not message:find(CP_SECRET_DOP, 1, true) then -- UTF-8 fallbacks if not (message:find('[Клады]', 1, true) and message:find('секретн', 1, true) and message:find('дополнительн', 1, true)) then return false end end local my = getMyNickname() if not my or my == '' then return false end -- extract nick after Кладоискатель local nick = message:match(CP_KLADOISK .. '%s+([^%[]+)%[%d+%]') or message:match('Кладоискатель%s+([^%[]+)%[%d+%]') if not nick then -- if message contains our nick somewhere near tag return message:find(my, 1, true) ~= nil end return normalizeName(nick) == normalizeName(my) end local function handleDopServerMessage(message) if not cfg.dopki or not cfg.scriptEnabled then return end message = tostring(message or '') if message:find(CP_SUCCESS_DIG, 1, true) or message:find('успешно достал', 1, true) or message:find('успешно достали', 1, true) then local loc = dopDigLocation or (function() local x, y, z = getPos(); if x then return { x = x, y = y, z = z } end end)() if loc then removeDopNear(loc.x, loc.y, loc.z, true) end return end if isOwnDopMessage(message) then if os.clock() - (tonumber(dopCefDetectedAt) or 0) <= 5.0 then return end -- remove dig-near active dop if any if dopDigLocation then removeDopNear(dopDigLocation.x, dopDigLocation.y, dopDigLocation.z, true) end startExpectDopCheckpoint() local cp = lastRaceCheckpoint if cp and cp.t and os.clock() - cp.t <= 1.5 then acceptDopCheckpoint(cp) end notify('dop: waiting checkpoint') end end ---------------------------------------------------------------------- -- digCursor — exact algorithm from original (no custom tweaks) ---------------------------------------------------------------------- local ffi_ok, ffi = pcall(require, 'ffi') local user32 = nil if ffi_ok and ffi then pcall(function() ffi.cdef[[ typedef struct GK_POINT { long x; long y; } GK_POINT; typedef struct GK_RECT { long left; long top; long right; long bottom; } GK_RECT; void *GetForegroundWindow(void); int GetClientRect(void *hwnd, GK_RECT *rect); int ClientToScreen(void *hwnd, GK_POINT *point); int SetCursorPos(int x, int y); int ClipCursor(const GK_RECT *rect); ]] end) pcall(function() user32 = ffi.load('user32') end) end local digLockActive = false local digLockStartedAt = 0 local digLockCursorSeenAt = 0 local digLockWindow = nil local digLockClipped = false local function releaseDigClip() if user32 ~= nil and digLockClipped then pcall(user32.ClipCursor, nil) end digLockClipped = false end local function stopDigCursorLock() digLockActive = false digLockStartedAt = 0 digLockCursorSeenAt = 0 digLockWindow = nil releaseDigClip() end local function startDigCursorLock() if not cfg.scriptEnabled or not cfg.digCursor then stopDigCursorLock() return false end if user32 == nil then return false end digLockActive = true digLockStartedAt = os.clock() digLockCursorSeenAt = 0 local ok, hwnd = pcall(user32.GetForegroundWindow) digLockWindow = ok and hwnd or nil return true end local function updateDigCursorLock(nowClock) if not cfg.digCursor then stopDigCursorLock() return end if not digLockActive or user32 == nil then return end nowClock = tonumber(nowClock) or os.clock() if nowClock - (tonumber(digLockStartedAt) or 0) > 45.0 then stopDigCursorLock() 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 digLockCursorSeenAt = nowClock elseif digLockCursorSeenAt > 0 and nowClock - digLockCursorSeenAt > 0.35 then stopDigCursorLock() return end local okForeground, foreground = pcall(user32.GetForegroundWindow) if not okForeground or foreground == nil or digLockWindow == nil or foreground ~= digLockWindow then releaseDigClip() return end local rect = ffi.new('GK_RECT[1]') local point = ffi.new('GK_POINT[1]') local okRect, hasRect = pcall(user32.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(user32.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('GK_RECT[1]') clip[0].left = centerX clip[0].top = centerY clip[0].right = centerX + 1 clip[0].bottom = centerY + 1 pcall(user32.SetCursorPos, centerX, centerY) local okClip, clipped = pcall(user32.ClipCursor, clip) digLockClipped = 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('GK_RECT[1]') clip[0].left = centerX clip[0].top = centerY clip[0].right = centerX + 1 clip[0].bottom = centerY + 1 pcall(user32.SetCursorPos, centerX, centerY) local okClip, clipped = pcall(user32.ClipCursor, clip) digLockClipped = okClip and clipped ~= 0 end end end local function digCursorTick(now) updateDigCursorLock(now) end local function isPreDigMessage(message) message = tostring(message or '') local my = getMyNickname() or '' if my == '' then return false end -- только своё сообщение о начале копания if not message:find(my, 1, true) then if not message:lower():find(my:lower(), 1, true) then return false end end local hasStart = message:find(CP_NACHAL, 1, true) or message:find('начал', 1, true) or message:find('Начал', 1, true) local hasDig = message:find(CP_KOPAT, 1, true) or message:find('копат', 1, true) or message:find('Копат', 1, true) return hasStart and hasDig end --- Успешная выкопка ТОЛЬКО своего персонажа (как в Nakhodka) local function isOwnSuccessDigMessage(message) message = tostring(message or '') local my = getMyNickname() if not my or my == '' then return false end if not message:find(my, 1, true) then if not message:lower():find(my:lower(), 1, true) then return false end end -- паттерн: Кладоискатель Nick[id] успешно достал ... if message:find(CP_SUCCESS_DIG, 1, true) or message:find('успешно достал', 1, true) or message:find('успешно достали', 1, true) then local nick = message:match(CP_KLADOISK .. '%s+([^%[%s]+)%[%d+%]') or message:match('Кладоискатель%s+([^%[%s]+)%[%d+%]') if nick then return normalizeName(nick) == normalizeName(my) end -- fallback: ник уже проверен выше return true end return false end function sampev.onServerMessage(color, message) pcall(handleDopServerMessage, message) message = tostring(message or '') if isOwnSuccessDigMessage(message) then if digLockActive then stopDigCursorLock() end -- считаем только если сами начинали копать (pending) — чужие копатели не попадают if cfg.counterEnabled and os.clock() <= countDigPendingUntil then countCompletedKlad() countDigPendingUntil = 0 end end if isPreDigMessage(message) then countDigPendingUntil = os.clock() + 120.0 pcall(captureDopDigContext) startDigCursorLock() updateDigCursorLock(os.clock()) end end -- Blinking treasure zone capture (original Nakhodka logic) function sampev.onCreateGangZone(zoneId, squareStart, squareEnd, color) local id = tonumber(zoneId) or zoneId if gangZoneLocalAdding[id] then return end local col = tonumber(color) local isKlad = (tonumber(zoneId) == kladZoneServerId and col == -16776961) or (tonumber(zoneId) == kladZoneServerId) if not isKlad then return end local l = squareStart and (squareStart.x or squareStart[1]) local u = squareStart and (squareStart.y or squareStart[2]) local r = squareEnd and (squareEnd.x or squareEnd[1]) local d = squareEnd and (squareEnd.y or squareEnd[2]) if not (l and u and r and d) then return end if not cfg.zoneOps then return end setKladZone(l, u, r, d) -- remove server blinking zone if option enabled, keep stable visual 610 lua_thread.create(function() wait(0) if cfg.disableZoneBlink then pcall(removeGangZone, kladZoneServerId) end drawSavedZoneVisual() end) notify(string.format('zone captured %.0f,%.0f — %.0f,%.0f', l, u, r, d)) if cfg.pointcheck then pcall(refreshBlips) end if cfg.markers3d then pcall(refreshMarkers3d) end end function sampev.onGangZoneDestroy(zoneId) local id = tonumber(zoneId) or zoneId if gangZoneLocalRemoving[id] then return end if tonumber(zoneId) == kladZoneServerId then -- server removed blink zone — keep ours fixed if zoneActive then lua_thread.create(function() wait(0) drawSavedZoneVisual() end) if cfg.pointcheck then pcall(refreshBlips) end end elseif tonumber(zoneId) == zoneVisualId and zoneActive then -- something removed our visual — put it back lua_thread.create(function() wait(0) drawSavedZoneVisual() end) end end function sampev.onInitGame() zoneNeedsRestore = true zoneRestoreAt = os.clock() + 2 end ---------------------------------------------------------------------- -- ImGui dark UI (Arizona-like) ---------------------------------------------------------------------- local menuOpen, activeTab, imguiReady, imguiInitTried = nil, 1, false, false local ui = {} ui.forceWindowSizeReset = false local COL = { bg = {0.05, 0.05, 0.05, 0.98}, child = {0.10, 0.10, 0.10, 1.00}, sidebar = {0.07, 0.07, 0.07, 1.00}, accent = {0.22, 0.78, 0.42, 1.00}, accentH = {0.30, 0.88, 0.50, 1.00}, accentA = {0.14, 0.58, 0.30, 1.00}, accentDim= {0.16, 0.45, 0.28, 1.00}, danger = {0.78, 0.24, 0.26, 1.00}, dangerH = {0.92, 0.32, 0.34, 1.00}, dangerA = {0.65, 0.18, 0.20, 1.00}, text = {0.94, 0.94, 0.95, 1.00}, muted = {0.50, 0.50, 0.54, 1.00}, frame = {0.16, 0.16, 0.18, 1.00}, frameH = {0.22, 0.22, 0.25, 1.00}, frameA = {0.14, 0.14, 0.16, 1.00}, border = {0.24, 0.24, 0.27, 1.00}, gold = {0.95, 0.82, 0.35, 1.00}, } local function V4(c) return imgui.ImVec4(c[1], c[2], c[3], c[4]) end -- styled buttons local function btnPush(kind) -- kind: 'default' | 'primary' | 'danger' | 'ghost' if kind == 'primary' then imgui.PushStyleColor(imgui.Col.Button, V4(COL.accent)) imgui.PushStyleColor(imgui.Col.ButtonHovered, V4(COL.accentH)) imgui.PushStyleColor(imgui.Col.ButtonActive, V4(COL.accentA)) imgui.PushStyleColor(imgui.Col.Text, imgui.ImVec4(0.05, 0.08, 0.05, 1)) return 4 elseif kind == 'danger' then imgui.PushStyleColor(imgui.Col.Button, V4(COL.frame)) imgui.PushStyleColor(imgui.Col.ButtonHovered, V4(COL.dangerH)) imgui.PushStyleColor(imgui.Col.ButtonActive, V4(COL.dangerA)) return 3 elseif kind == 'ghost' then imgui.PushStyleColor(imgui.Col.Button, imgui.ImVec4(0, 0, 0, 0)) imgui.PushStyleColor(imgui.Col.ButtonHovered, V4(COL.frameH)) imgui.PushStyleColor(imgui.Col.ButtonActive, V4(COL.frameA)) return 3 elseif kind == 'hover_green' then -- standard button, green on hover imgui.PushStyleColor(imgui.Col.Button, V4(COL.frame)) imgui.PushStyleColor(imgui.Col.ButtonHovered, V4(COL.accentH)) imgui.PushStyleColor(imgui.Col.ButtonActive, V4(COL.accentA)) return 3 else imgui.PushStyleColor(imgui.Col.Button, V4(COL.frame)) imgui.PushStyleColor(imgui.Col.ButtonHovered, V4(COL.frameH)) imgui.PushStyleColor(imgui.Col.ButtonActive, V4(COL.accentA)) return 3 end end local function btnPop(n) imgui.PopStyleColor(n or 3) end local function niceButton(label, size, kind) local n = btnPush(kind or 'default') local clicked = imgui.Button(label, size or imgui.ImVec2(0, 0)) btnPop(n) return clicked end local function syncUi() if not imguiReady then return end pcall(function() ui.scriptEnabled[0] = cfg.scriptEnabled and true or false ui.pointcheck[0] = cfg.pointcheck and true or false ui.markers3d[0] = cfg.markers3d and true or false ui.dopki[0] = cfg.dopki and true or false ui.digCursor[0] = cfg.digCursor and true or false ui.onlyInZone[0] = cfg.onlyInZone and true or false ui.keepCheckedBlips[0] = cfg.keepCheckedBlips and true or false ui.zoneOps[0] = cfg.zoneOps and true or false ui.disableZoneBlink[0] = cfg.disableZoneBlink and true or false ui.showCircles[0] = cfg.showCircles and true or false ui.showArrows[0] = cfg.showArrows and true or false if ui.counterEnabled then ui.counterEnabled[0] = cfg.counterEnabled ~= false end if ui.showCounter then ui.showCounter[0] = cfg.showCounter ~= false end if ui.counterPosX then ui.counterPosX[0] = math.floor(tonumber(cfg.counterPosX) or 1750) end if ui.counterPosY then ui.counterPosY[0] = math.floor(tonumber(cfg.counterPosY) or 250) end ui.maxBlips[0] = math.floor(tonumber(cfg.maxBlips) or 50) ui.maxMarkers3d[0] = math.floor(tonumber(cfg.maxMarkers3d) or 40) ui.maxCircles[0] = math.floor(tonumber(cfg.maxCircles) or 40) ui.checkRadius[0] = tonumber(cfg.checkRadius) or 3.0 ui.circleRadius[0] = tonumber(cfg.circleRadius) or 1.4 if ui.blipSprite then ui.blipSprite[0] = math.floor(tonumber(cfg.blipSprite) or 56) end end) end local function sectionTitle(s) imgui.Dummy(imgui.ImVec2(0, 2)) imgui.TextColored(V4(COL.text), T(s)) imgui.PushStyleColor(imgui.Col.Separator, V4(COL.accentDim)) imgui.Separator() imgui.PopStyleColor() imgui.Dummy(imgui.ImVec2(0, 6)) end local function renderMenu() if not menuOpen or not menuOpen[0] then return end -- default size 950x650: центр только при первом открытии и при «Восстановить стандартные настройки» local ww, wh = 950, 650 local sizeCond = imgui.Cond.FirstUseEver if ui.forceWindowSizeReset then sizeCond = imgui.Cond.Always ui.forceWindowSizeReset = false end if type(getScreenResolution) == 'function' then local ok, sw, sh = pcall(getScreenResolution) if ok and sw and sh then if ww > sw * 0.9 then ww = math.floor(sw * 0.9) end if wh > sh * 0.9 then wh = math.floor(sh * 0.9) end imgui.SetNextWindowSize(imgui.ImVec2(ww, wh), sizeCond) imgui.SetNextWindowPos(imgui.ImVec2((sw - ww) * 0.5, (sh - wh) * 0.5), sizeCond) else imgui.SetNextWindowSize(imgui.ImVec2(ww, wh), sizeCond) end else imgui.SetNextWindowSize(imgui.ImVec2(ww, wh), sizeCond) end -- rounding / spacing local varCount = 0 if imgui.PushStyleVarFloat then imgui.PushStyleVarFloat(imgui.StyleVar.FrameRounding, 6) imgui.PushStyleVarFloat(imgui.StyleVar.WindowRounding, 10) imgui.PushStyleVarFloat(imgui.StyleVar.ChildRounding, 8) imgui.PushStyleVarFloat(imgui.StyleVar.GrabRounding, 4) imgui.PushStyleVarFloat(imgui.StyleVar.FrameBorderSize, 0) varCount = 5 end if imgui.PushStyleVarVec2 then imgui.PushStyleVarVec2(imgui.StyleVar.FramePadding, imgui.ImVec2(12, 7)) imgui.PushStyleVarVec2(imgui.StyleVar.ItemSpacing, imgui.ImVec2(10, 8)) imgui.PushStyleVarVec2(imgui.StyleVar.WindowPadding, imgui.ImVec2(12, 10)) varCount = varCount + 3 end local black = imgui.ImVec4(0.04, 0.04, 0.04, 1.00) imgui.PushStyleColor(imgui.Col.WindowBg, V4(COL.bg)) imgui.PushStyleColor(imgui.Col.ChildBg, V4(COL.child)) imgui.PushStyleColor(imgui.Col.Border, V4(COL.border)) imgui.PushStyleColor(imgui.Col.Text, V4(COL.text)) imgui.PushStyleColor(imgui.Col.Button, V4(COL.frame)) imgui.PushStyleColor(imgui.Col.ButtonHovered, V4(COL.frameH)) imgui.PushStyleColor(imgui.Col.ButtonActive, V4(COL.accentA)) imgui.PushStyleColor(imgui.Col.FrameBg, V4(COL.frame)) imgui.PushStyleColor(imgui.Col.FrameBgHovered, V4(COL.frameH)) imgui.PushStyleColor(imgui.Col.CheckMark, V4(COL.accent)) imgui.PushStyleColor(imgui.Col.SliderGrab, V4(COL.accent)) imgui.PushStyleColor(imgui.Col.SliderGrabActive, V4(COL.accentH)) imgui.PushStyleColor(imgui.Col.Header, V4(COL.frame)) imgui.PushStyleColor(imgui.Col.HeaderHovered, V4(COL.accent)) imgui.PushStyleColor(imgui.Col.Separator, V4(COL.border)) -- верхняя полоса / title: чёрный вместо синего if imgui.Col.TitleBg then imgui.PushStyleColor(imgui.Col.TitleBg, black) end if imgui.Col.TitleBgActive then imgui.PushStyleColor(imgui.Col.TitleBgActive, black) end if imgui.Col.TitleBgCollapsed then imgui.PushStyleColor(imgui.Col.TitleBgCollapsed, black) end if imgui.Col.MenuBarBg then imgui.PushStyleColor(imgui.Col.MenuBarBg, black) end local styleCount = 15 + (imgui.Col.TitleBg and 1 or 0) + (imgui.Col.TitleBgActive and 1 or 0) + (imgui.Col.TitleBgCollapsed and 1 or 0) + (imgui.Col.MenuBarBg and 1 or 0) local open = imgui.Begin(T('##GoldenKladMain'), menuOpen) if not open then imgui.End() imgui.PopStyleColor(styleCount) if varCount and varCount > 0 then imgui.PopStyleVar(varCount) end return end -- LEFT SIDEBAR imgui.PushStyleColor(imgui.Col.ChildBg, V4(COL.sidebar)) imgui.BeginChild('##sidebar', imgui.ImVec2(186, -44), true) -- brand block (full green, larger, centered) imgui.Dummy(imgui.ImVec2(0, 10)) if imgui.SetWindowFontScale then imgui.SetWindowFontScale(1.35) end do local label = 'Golden Klad' local tw = imgui.CalcTextSize(label).x local avail = imgui.GetContentRegionAvail().x local cx = imgui.GetCursorPosX() imgui.SetCursorPosX(cx + math.max(0, (avail - tw) * 0.5)) -- золотой бренд (как логотип) imgui.TextColored(imgui.ImVec4(0.91, 0.72, 0.14, 1), label) end if imgui.SetWindowFontScale then imgui.SetWindowFontScale(1.0) end imgui.Dummy(imgui.ImVec2(0, 6)) imgui.PushStyleColor(imgui.Col.Separator, V4(COL.accentDim)) imgui.Separator() imgui.PopStyleColor() imgui.Dummy(imgui.ImVec2(0, 8)) local tabs = { { T('Основное'), 1 }, { T('Точки на карте'), 2 }, { T('Работа с зоной'), 3 }, { T('3D метки'), 4 }, { T('Доп. Клады'), 5 }, { T('Подсчёт кладов'), 6 }, { T('Активация'), 7 }, } for _, tab in ipairs(tabs) do if activeTab == tab[2] then if niceButton(tab[1] .. '##s' .. tab[2], imgui.ImVec2(-1, 30), 'primary') then activeTab = tab[2] end else if niceButton(tab[1] .. '##s' .. tab[2], imgui.ImVec2(-1, 30), 'default') then activeTab = tab[2] end end imgui.Dummy(imgui.ImVec2(0, 2)) end imgui.EndChild() imgui.PopStyleColor() -- ChildBg imgui.SameLine() -- CONTENT imgui.BeginChild('##content', imgui.ImVec2(0, -44), true) if activeTab == 1 then sectionTitle(T('МОДУЛИ')) if imgui.Checkbox(T('Включить скрипт'), ui.scriptEnabled) then cfg.scriptEnabled = ui.scriptEnabled[0]; saveSettings() if not cfg.scriptEnabled then clearMainBlips(); clearMarkers3d(); clearDopBlips() else refreshBlips(); refreshDopBlips(); refreshMarkers3d() end end if imgui.Checkbox(T('Точки на карте'), ui.pointcheck) then cfg.pointcheck = ui.pointcheck[0]; saveSettings() if cfg.pointcheck then refreshBlips() else clearMainBlips() end end if imgui.Checkbox(T('Работа с зоной'), ui.zoneOps) then cfg.zoneOps = ui.zoneOps[0]; saveSettings() if not cfg.zoneOps then pcall(removeGangZone, zoneVisualId) elseif zoneActive then drawSavedZoneVisual() end end if imgui.Checkbox(T('3D метки'), ui.markers3d) then cfg.markers3d = ui.markers3d[0]; saveSettings() if cfg.markers3d then refreshMarkers3d() else clearMarkers3d() end end if imgui.Checkbox(T('Доп. Клады'), ui.dopki) then cfg.dopki = ui.dopki[0]; saveSettings() if cfg.dopki then refreshDopBlips() else clearDopBlips() end end if ui.counterEnabled and imgui.Checkbox(T('Подсчёт кладов'), ui.counterEnabled) then cfg.counterEnabled = ui.counterEnabled[0]; saveSettings() end if imgui.Checkbox(T('Фиксированный курсор при копании'), ui.digCursor) then cfg.digCursor = ui.digCursor[0]; saveSettings() end imgui.Dummy(imgui.ImVec2(0, 10)) sectionTitle(T('ПОДПИСКА')) do local flags = 0 if imgui.WindowFlags and imgui.WindowFlags.NoScrollbar then flags = imgui.WindowFlags.NoScrollbar end if licenseIsActive() then local left = formatTimeLeft(cfg.licenseExpires) local src = tostring(cfg.licenseSource or '') local srcLabel = (src == 'boosty' and 'Boosty') or (src == 'funpay' and 'FunPay') or '' imgui.BeginChild('##sub_status', imgui.ImVec2(-1, 92), true, flags) if srcLabel ~= '' then imgui.TextColored(V4(COL.muted), T('Источник:')) imgui.SameLine(0, 6) imgui.TextColored(V4(COL.accent), srcLabel) end imgui.TextColored(V4(COL.accent), T('Осталось')) if left ~= '' then if imgui.SetWindowFontScale then imgui.SetWindowFontScale(1.25) end imgui.TextColored(V4(COL.accent), left) if imgui.SetWindowFontScale then imgui.SetWindowFontScale(1.0) end else imgui.TextColored(V4(COL.accent), T('—')) end imgui.EndChild() else imgui.BeginChild('##sub_status', imgui.ImVec2(-1, 56), true, flags) imgui.TextColored(imgui.ImVec4(0.95, 0.30, 0.28, 1), T('Не активирована')) imgui.TextColored(V4(COL.muted), T('Открой вкладку Активация')) imgui.EndChild() end end imgui.Dummy(imgui.ImVec2(0, 8)) if niceButton(T('Перезагрузить скрипт'), imgui.ImVec2(-1, 32), 'hover_green') then pcall(function() ensureConfigDir() writeFile(join(CONFIG_DIR, 'gk_reopen.flag'), '1') end) chatMsg('Скрипт перезагружен.') if thisScript and type(thisScript) == 'function' then local s = thisScript() if s and type(s.reload) == 'function' then lua_thread.create(function() wait(50) s:reload() end) end end end elseif activeTab == 2 then sectionTitle(T('ТОЧКИ НА КАРТЕ')) local cnt = 0; for _ in pairs(checked) do cnt = cnt + 1 end imgui.Text(string.format(T('Всего точек: %d'), #mainPoints)) imgui.Text(string.format(T('Отмечено: %d'), cnt)) imgui.Dummy(imgui.ImVec2(0, 6)) if imgui.SliderInt(T('Лимит блипов'), ui.maxBlips, 10, 200) then cfg.maxBlips = ui.maxBlips[0]; saveSettings(); refreshBlips() end if imgui.SliderFloat(T('Радиус проверки'), ui.checkRadius, 0.5, 8.0) then cfg.checkRadius = ui.checkRadius[0]; saveSettings() end if imgui.SliderInt(T('Иконка точки на карте'), ui.blipSprite, 1, 63) then cfg.blipSprite = ui.blipSprite[0] saveSettings() refreshBlips() end imgui.TextColored(V4(COL.muted), T('Иконка основных кладов на миникарте. По умолчанию 56.')) imgui.Dummy(imgui.ImVec2(0, 6)) if imgui.Checkbox(T('Точки на карте всегда'), ui.keepCheckedBlips) then cfg.keepCheckedBlips = ui.keepCheckedBlips[0] saveSettings() refreshBlips() end imgui.TextColored(V4(COL.muted), T('Проверенные блипы не исчезают с карты')) imgui.Dummy(imgui.ImVec2(0, 6)) if niceButton(T('Сбросить отмеченные'), imgui.ImVec2(-1, 30), 'hover_green') then checked = {} refreshBlips() refreshMarkers3d() chatMsg('Сброс завершен.') end elseif activeTab == 3 then sectionTitle(T('РАБОТА С ЗОНОЙ')) if imgui.Checkbox(T('Операции с зоной'), ui.zoneOps) then cfg.zoneOps = ui.zoneOps[0]; saveSettings() if not cfg.zoneOps then pcall(removeGangZone, zoneVisualId) elseif zoneActive then drawSavedZoneVisual() end end if imgui.Checkbox(T('Отключить мигающую зону'), ui.disableZoneBlink) then cfg.disableZoneBlink = ui.disableZoneBlink[0]; saveSettings() end imgui.TextColored(V4(COL.muted), T('Серверная мигающая зона заменяется стабильной.')) if imgui.Checkbox(T('Точки только в зоне клада'), ui.onlyInZone) then cfg.onlyInZone = ui.onlyInZone[0]; saveSettings() if cfg.onlyInZone and not zoneActive then notify(T('Зона ещё не поймана — открой карту клада на сервере')) end refreshBlips() end imgui.Dummy(imgui.ImVec2(0, 6)) sectionTitle(T('СТАТУС')) if zoneActive and zone then imgui.TextColored(V4(COL.accent), T('Зона активна')) imgui.Text(string.format('L: %.1f U: %.1f', zone.left, zone.up)) imgui.Text(string.format('R: %.1f D: %.1f', zone.right, zone.down)) else imgui.TextColored(V4(COL.danger), T('Зона не активна')) end imgui.Dummy(imgui.ImVec2(0, 8)) sectionTitle(T('КОМАНДЫ')) imgui.Columns(2, '##zone_cmds', false) imgui.SetColumnWidth(0, 160) imgui.TextColored(V4(COL.accent), '/zd /zonedel') imgui.NextColumn() imgui.TextWrapped(T('Удалить активную зону с карты')) imgui.NextColumn() imgui.TextColored(V4(COL.accent), '/zc /zonecopy') imgui.NextColumn() imgui.TextWrapped(T('Скопировать текущую зону в буфер')) imgui.NextColumn() imgui.TextColored(V4(COL.accent), '/zp /zonepaste') imgui.NextColumn() imgui.TextWrapped(T('Вставить зону по координатам из буфера')) imgui.NextColumn() imgui.Columns(1) imgui.Dummy(imgui.ImVec2(0, 6)) if niceButton(T('Удалить зону сейчас'), imgui.ImVec2(-1, 30), 'danger') then if cfg.zoneOps then clearKladZone(false) if cfg.pointcheck then refreshBlips() end chatMsg('Зона удалена.') end end elseif activeTab == 4 then sectionTitle(T('3D МЕТКИ')) if imgui.Checkbox(T('3D Круги на земле'), ui.showCircles) then cfg.showCircles = ui.showCircles[0]; saveSettings() end imgui.Text(T('Круги: ')) imgui.SameLine(0, 0) imgui.TextColored(imgui.ImVec4(0.25, 0.85, 0.35, 1), T('зелёный')) imgui.SameLine(0, 0) imgui.Text(T(' — не проверен, ')) imgui.SameLine(0, 0) imgui.TextColored(imgui.ImVec4(0.95, 0.25, 0.25, 1), T('красный')) imgui.SameLine(0, 0) imgui.Text(T(' — проверен, ')) imgui.SameLine(0, 0) imgui.TextColored(imgui.ImVec4(1.0, 0.65, 0.15, 1), T('оранжевый')) imgui.SameLine(0, 0) imgui.Text(T(' — доп. клад')) if imgui.Checkbox(T('Стрелки на земле'), ui.showArrows) then cfg.showArrows = ui.showArrows[0]; saveSettings() if cfg.showArrows then refreshMarkers3d() else clearMarkers3d() end end imgui.Dummy(imgui.ImVec2(0, 6)) if imgui.SliderInt(T('Лимит кругов'), ui.maxCircles, 5, 60) then cfg.maxCircles = ui.maxCircles[0]; saveSettings() end if imgui.SliderFloat(T('Радиус круга'), ui.circleRadius, 0.4, 4.0) then cfg.circleRadius = ui.circleRadius[0]; saveSettings() end if imgui.SliderInt(T('Лимит стрелок'), ui.maxMarkers3d, 5, 80) then cfg.maxMarkers3d = ui.maxMarkers3d[0]; saveSettings(); refreshMarkers3d() end imgui.Dummy(imgui.ImVec2(0, 6)) if niceButton(T('Обновить 3D Рендеринг'), imgui.ImVec2(-1, 32), 'primary') then local n, err = loadCoords() if err then notify('coords: ' .. tostring(err)) end refreshBlips() refreshMarkers3d() refreshDopBlips() chatMsg('3D рендеринг обновлен.') end imgui.TextColored(V4(COL.muted), T('Круги рисуются каждый кадр, если точка на экране.')) elseif activeTab == 5 then sectionTitle(T('ДОП. КЛАДЫ')) if imgui.Checkbox(T('Включить Доп. Клады'), ui.dopki) then cfg.dopki = ui.dopki[0]; saveSettings() if not cfg.dopki then for _, d in ipairs(dopPoints) do d.showOnMap = false end clearDopBlips() end end imgui.TextColored(V4(COL.muted), T('Запись без меток. Метка — только по кнопке «Найти».')) if niceButton(T('Добавить Доп. Клад здесь'), imgui.ImVec2(-1, 32), 'default') then notify(addDopHere() and T('Доп. Клад добавлен') or T('ошибка')) end if niceButton(T('Скрыть все метки'), imgui.ImVec2(-1, 28), 'default') then for _, d in ipairs(dopPoints) do d.showOnMap = false safeRemoveBlip(d.blip); d.blip = nil end clearDopNavigator() saveDopki() end imgui.Dummy(imgui.ImVec2(0, 4)) imgui.Text(T('Список Доп. Кладов: ') .. tostring(#dopPoints)) imgui.BeginChild('##doplist', imgui.ImVec2(0, 0), true) for _, dp in ipairs(dopPoints) do local mark = dp.showOnMap and '[*] ' or '' if mark ~= '' then imgui.TextColored(V4(COL.accent), mark) imgui.SameLine(0, 0) end imgui.TextColored(imgui.ImVec4(0.95, 0.30, 0.30, 1), string.format('#%d', dp.id)) imgui.SameLine(0, 8) imgui.TextColored(imgui.ImVec4(0.25, 0.85, 0.35, 1), string.format('%.1f %.1f %.1f', dp.x, dp.y, dp.z)) imgui.SameLine() if dp.showOnMap then if imgui.Button(T('Скрыть') .. '##h' .. dp.id) then setDopOnMap(dp.id, false); break end else imgui.PushStyleColor(imgui.Col.ButtonHovered, imgui.ImVec4(0.20, 0.70, 0.32, 1)) imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.15, 0.55, 0.25, 1)) if imgui.Button(T('Найти') .. '##f' .. dp.id) then setDopOnMap(dp.id, true) imgui.PopStyleColor(2) break end imgui.PopStyleColor(2) end imgui.SameLine() if imgui.Button(T('Копировать') .. '##c' .. dp.id) then -- format compatible with /zp (zone paste around point) local pad = 15.0 local text = string.format('/zonepaste l: %.4f; u: %.4f; r: %.4f; d: %.4f', dp.x - pad, dp.y + pad, dp.x + pad, dp.y - pad) if type(setClipboardText) == 'function' then pcall(setClipboardText, text) end print('[Golden Klad] ' .. text) chatMsg('Скопировано. Вставь: /zp') break end imgui.SameLine() imgui.PushStyleColor(imgui.Col.ButtonHovered, V4(COL.danger)) imgui.PushStyleColor(imgui.Col.ButtonActive, imgui.ImVec4(0.75, 0.18, 0.18, 1)) if imgui.Button(T('Удалить') .. '##d' .. dp.id) then removeDop(dp.id) imgui.PopStyleColor(2) break end imgui.PopStyleColor(2) end imgui.EndChild() elseif activeTab == 6 then sectionTitle(T('ПОДСЧЁТ КЛАДОВ')) ensurePeriodStats() local green = imgui.ImVec4(0.18, 0.82, 0.40, 1) local yellow = imgui.ImVec4(0.95, 0.82, 0.25, 1) local red = imgui.ImVec4(0.95, 0.25, 0.25, 1) local totalN = math.floor(tonumber(kladCount.total) or 0) local todayN = math.floor(tonumber(kladCount.today) or 0) local weekN = math.floor(tonumber(kladCount.week) or 0) -- понятная сводка imgui.BeginChild('##klad_stats_box', imgui.ImVec2(-1, 118), true) imgui.Dummy(imgui.ImVec2(0, 4)) imgui.TextColored(green, T('За всё время: ') .. tostring(totalN)) imgui.Dummy(imgui.ImVec2(0, 2)) imgui.TextColored(yellow, T('За сегодня: ') .. tostring(todayN)) imgui.Dummy(imgui.ImVec2(0, 2)) imgui.TextColored(red, T('За неделю: ') .. tostring(weekN)) imgui.Dummy(imgui.ImVec2(0, 4)) imgui.TextColored(V4(COL.muted), T('Сегодня: ') .. tostring(kladCount.date or os.date('%Y-%m-%d'))) imgui.EndChild() imgui.Dummy(imgui.ImVec2(0, 8)) if ui.showCounter and imgui.Checkbox(T('Показывать на экране'), ui.showCounter) then cfg.showCounter = ui.showCounter[0]; saveSettings() end imgui.TextColored(V4(COL.muted), T('Позиция на экране')) if ui.counterPosX and imgui.SliderInt(T('X'), ui.counterPosX, 0, 1920) then cfg.counterPosX = ui.counterPosX[0]; saveSettings() end if ui.counterPosY and imgui.SliderInt(T('Y'), ui.counterPosY, 0, 1080) then cfg.counterPosY = ui.counterPosY[0]; saveSettings() end if niceButton(T('Сбросить позицию (1750, 250)'), imgui.ImVec2(-1, 28), 'default') then cfg.counterPosX = 1750; cfg.counterPosY = 250 if ui.counterPosX then ui.counterPosX[0] = 1750 end if ui.counterPosY then ui.counterPosY[0] = 250 end saveSettings() end imgui.Dummy(imgui.ImVec2(0, 8)) imgui.TextColored(V4(COL.muted), T('Сброс статистики')) if niceButton(T('Сбросить за сегодня'), imgui.ImVec2(-1, 28), 'default') then resetKladToday(); chatMsg('Счётчик за сегодня сброшен.') end if niceButton(T('Сбросить за неделю'), imgui.ImVec2(-1, 28), 'default') then resetKladWeek(); chatMsg('Счётчик за неделю сброшен.') end if niceButton(T('Сбросить всё'), imgui.ImVec2(-1, 28), 'danger') then resetKladAll(); chatMsg('Счётчик сброшен полностью.') end elseif activeTab == 7 then local src = tostring(cfg.licenseSource or '') local activeFp = licenseIsActive() and src == 'funpay' local activeBy = licenseIsActive() and src == 'boosty' local colTitleFp = activeFp and V4(COL.accent) or V4(COL.text) local colTitleBy = activeBy and V4(COL.accent) or V4(COL.text) sectionTitle(T('АКТИВАЦИЯ')) -- блок FunPay imgui.TextColored(colTitleFp, T('FUNPAY / ОБЫЧНЫЙ КЛЮЧ')) imgui.PushStyleColor(imgui.Col.Separator, activeFp and V4(COL.accent) or V4(COL.accentDim)) imgui.Separator() imgui.PopStyleColor() imgui.Dummy(imgui.ImVec2(0, 4)) imgui.TextColored(activeFp and V4(COL.accent) or V4(COL.muted), T('Ключ (FunPay / обычный)')) if ui.licenseKey then imgui.InputText('##licenseKey', ui.licenseKey, 128) end imgui.Dummy(imgui.ImVec2(0, 4)) if niceButton(T('Активировать ключ'), imgui.ImVec2(-1, 30), 'primary') then local key = ffi_string_safe(ui.licenseKey) cfg.funpayKey = key saveSettings() lua_thread.create(function() local ok, info = activateLicense(key) if not ok then chatMsg('Активация: ' .. tostring(info)) end if ui and ui.licenseKey then setCharBuf(ui.licenseKey, cfg.funpayKey or key, 128) end end) end imgui.Dummy(imgui.ImVec2(0, 12)) -- блок Boosty imgui.TextColored(colTitleBy, T('BOOSTY')) imgui.PushStyleColor(imgui.Col.Separator, activeBy and V4(COL.accent) or V4(COL.accentDim)) imgui.Separator() imgui.PopStyleColor() imgui.Dummy(imgui.ImVec2(0, 4)) imgui.TextColored(activeBy and V4(COL.accent) or V4(COL.muted), T('Почта (укажите почту на которую зарегистрирован аккаунт Boosty)')) if ui.licenseEmail then imgui.InputText('##licenseEmail', ui.licenseEmail, 128) end imgui.TextColored(activeBy and V4(COL.accent) or V4(COL.muted), T('Код периода (выдается на странице Boosty)')) if ui.periodCode then imgui.InputText('##periodCode', ui.periodCode, 128) end -- boostyLicenseKey хранится в cfg и используется логикой, но не показывается пользователю imgui.Dummy(imgui.ImVec2(0, 4)) if niceButton(T('Активировать / продлить код'), imgui.ImVec2(-1, 30), 'primary') then local em = ffi_string_safe(ui.licenseEmail) local pc = ffi_string_safe(ui.periodCode) cfg.licenseEmail = em cfg.periodCode = pc saveSettings() lua_thread.create(function() local ok, info = redeemPeriodCode(em, pc) if not ok then chatMsg('Boosty: ' .. tostring(info)) end if ui and ui.periodCode then setCharBuf(ui.periodCode, cfg.periodCode or pc, 128) end if ok and ui and ui.boostyLicenseKey then setCharBuf(ui.boostyLicenseKey, cfg.boostyLicenseKey or '', 128) end end) end imgui.Dummy(imgui.ImVec2(0, 10)) sectionTitle(T('СТАТУС ПОДПИСКИ')) if licenseIsActive() then local untilStr = formatExpiresDisplay(cfg.licenseExpires) local left = formatTimeLeft(cfg.licenseExpires) local red = imgui.ImVec4(0.95, 0.28, 0.28, 1) local srcLabel = (src == 'boosty' and 'Boosty') or (src == 'funpay' and 'FunPay') or '' local statusText = srcLabel ~= '' and (T('Статус: активна (') .. srcLabel .. ')') or T('Статус: активна') local flags = 0 if imgui.WindowFlags and imgui.WindowFlags.NoScrollbar then flags = imgui.WindowFlags.NoScrollbar end imgui.BeginChild('##act_status', imgui.ImVec2(-1, 86), true, flags) imgui.TextColored(V4(COL.accent), statusText) if untilStr ~= '' then imgui.TextColored(V4(COL.accent), T('Подписка до ')) imgui.SameLine(0, 0) imgui.TextColored(V4(COL.accent), untilStr) end if left ~= '' then imgui.TextColored(V4(COL.accent), T('Осталось ')) imgui.SameLine(0, 0) imgui.TextColored(V4(COL.accent), left) end imgui.EndChild() else local flags = 0 if imgui.WindowFlags and imgui.WindowFlags.NoScrollbar then flags = imgui.WindowFlags.NoScrollbar end imgui.BeginChild('##act_status', imgui.ImVec2(-1, 40), true, flags) imgui.TextColored(imgui.ImVec4(0.95, 0.35, 0.30, 1), T('Подписка не активна')) imgui.EndChild() end do local remote = tostring(pendingScriptUpdate or '') local localVer = localScriptVersion() if remote == '' and cfg.scriptRemoteVersion and versionIsNewer(cfg.scriptRemoteVersion, localVer) then remote = tostring(cfg.scriptRemoteVersion) pendingScriptUpdate = remote end if remote ~= '' and versionIsNewer(remote, localVer) then imgui.Dummy(imgui.ImVec2(0, 8)) imgui.TextColored(V4(COL.accent), T('Доступна новая версия: ') .. remote) if niceButton(T('Обновить скрипт в игре'), imgui.ImVec2(-1, 30), 'primary') then lua_thread.create(function() chatMsg('Загрузка версии ' .. remote .. '...') local ok, err = downloadAndApplyScriptUpdate() if not ok then chatMsg('Обновление не удалось: ' .. tostring(err)) updateFooterNotice = { kind = 'err', text = 'Ошибка обновления', expiresAt = os.time() + 20, } end end) end end end if ui.autoUpdate then imgui.Dummy(imgui.ImVec2(0, 6)) if imgui.Checkbox(T('Автоматическое обновление'), ui.autoUpdate) then cfg.autoUpdate = ui.autoUpdate[0]; saveSettings() end end end imgui.EndChild() -- content -- footer buttons if niceButton(T('Восстановить стандартные настройки'), imgui.ImVec2(300, 32), 'danger') then cfg.scriptEnabled = true cfg.pointcheck = true cfg.markers3d = true cfg.dopki = true cfg.digCursor = true cfg.onlyInZone = false cfg.keepCheckedBlips = false cfg.zoneOps = true cfg.disableZoneBlink = true cfg.showCircles = true cfg.showArrows = true cfg.maxBlips = 50 cfg.blipSprite = 56 cfg.checkRadius = 3.0 cfg.maxCircles = 40 cfg.maxMarkers3d = 40 cfg.circleRadius = 1.4 cfg.dopExpireHours = 3 ui.forceWindowSizeReset = true saveSettings(); syncUi() refreshBlips(); refreshMarkers3d(); refreshDopBlips() chatMsg('Установлены настройки по умолчанию.') end -- brand + update notice bottom-right local winW = imgui.GetWindowSize().x local ver = localScriptVersion() local left = 'Golden Klad' local right = 'v' .. ver local notice = updateFooterNotice if notice and notice.expiresAt and os.time() > (tonumber(notice.expiresAt) or 0) then if notice.kind ~= 'avail' then updateFooterNotice = nil notice = nil end end -- доступное обновление держим, пока не обновят if (not notice or notice.kind ~= 'ok') and pendingScriptUpdate then local lv = localScriptVersion() if versionIsNewer(pendingScriptUpdate, lv) then notice = { kind = 'avail', text = 'Доступно v' .. tostring(pendingScriptUpdate), expiresAt = os.time() + 86400, } updateFooterNotice = notice end end local line2 = '' local col2 = V4(COL.muted) if notice and notice.text then line2 = tostring(notice.text) if notice.kind == 'ok' then col2 = V4(COL.accent) -- зелёный elseif notice.kind == 'err' then col2 = imgui.ImVec4(0.95, 0.32, 0.30, 1) else col2 = imgui.ImVec4(0.95, 0.55, 0.20, 1) -- оранжевый: доступно end end local full = left .. ' · ' .. right if line2 ~= '' then full = full .. ' | ' .. line2 end local aw = imgui.CalcTextSize(full).x imgui.SameLine() imgui.SetCursorPosX(math.max(320, winW - aw - 18)) imgui.TextColored(V4(COL.muted), left) imgui.SameLine(0, 0) imgui.TextColored(V4(COL.muted), ' · ' .. right) if line2 ~= '' then imgui.SameLine(0, 0) imgui.TextColored(V4(COL.muted), ' | ') imgui.SameLine(0, 0) imgui.TextColored(col2, line2) end imgui.End() imgui.PopStyleColor(styleCount) if varCount and varCount > 0 then imgui.PopStyleVar(varCount) end end local menuCursorOwned = false local function setMenuCursor(enabled) if type(showCursor) ~= 'function' then return end if enabled then local ok = pcall(showCursor, true, false) if not ok then pcall(showCursor, true) end menuCursorOwned = true else if not menuCursorOwned then return end local ok = pcall(showCursor, false, false) if not ok then pcall(showCursor, false) end menuCursorOwned = false end end local function initImgui() if imguiInitTried then return imguiReady end imguiInitTried = true if not imgui_ok or not imgui or not new then notify(T('mimgui не найден')) return false end local ok, err = pcall(function() menuOpen = new.bool(false) ui.scriptEnabled = new.bool(true) ui.pointcheck = new.bool(true) ui.markers3d = new.bool(true) ui.dopki = new.bool(true) ui.digCursor = new.bool(true) ui.onlyInZone = new.bool(false) ui.keepCheckedBlips = new.bool(false) ui.zoneOps = new.bool(true) ui.disableZoneBlink = new.bool(true) ui.showCircles = new.bool(true) ui.showArrows = new.bool(true) ui.counterEnabled = new.bool(cfg.counterEnabled ~= false) ui.showCounter = new.bool(cfg.showCounter ~= false) ui.counterPosX = new.int(math.floor(tonumber(cfg.counterPosX) or 1750)) ui.counterPosY = new.int(math.floor(tonumber(cfg.counterPosY) or 250)) ui.maxBlips = new.int(50) ui.blipSprite = new.int(56) ui.maxMarkers3d = new.int(40) ui.maxCircles = new.int(40) ui.checkRadius = new.float(3.0) ui.circleRadius = new.float(1.4) ui.autoUpdate = new.bool(cfg.autoUpdate ~= false) ui.licenseKey = new.char[128]() ui.licenseEmail = new.char[128]() ui.periodCode = new.char[128]() ui.boostyLicenseKey = new.char[128]() ui.apiBase = new.char[256]() -- FunPay-поле: только ключ FunPay, не служебный ключ Boosty setCharBuf(ui.licenseKey, (cfg.funpayKey ~= nil and cfg.funpayKey ~= '') and cfg.funpayKey or '', 128) if ui.licenseEmail then setCharBuf(ui.licenseEmail, cfg.licenseEmail or '', 128) end if ui.periodCode then setCharBuf(ui.periodCode, cfg.periodCode or '', 128) end if ui.boostyLicenseKey then setCharBuf(ui.boostyLicenseKey, cfg.boostyLicenseKey or '', 128) end setCharBuf(ui.apiBase, cfg.apiBase, 256) imgui.OnFrame(function() local open = menuOpen ~= nil and menuOpen[0] == true if open then setMenuCursor(true) end return open end, function() local rok, rerr = pcall(renderMenu) if not rok then menuOpen[0] = false setMenuCursor(false) print('[Golden Klad] UI: ' .. tostring(rerr)) end end) end) if not ok then print('[Golden Klad] imgui init: ' .. tostring(err)) notify(T('ошибка imgui')) return false end imguiReady = true syncUi() return true end local function toggleMenu() if not imguiReady then notify(T('imgui не готов')); return end local opening = not menuOpen[0] menuOpen[0] = not menuOpen[0] if opening then activeTab = 1 -- always open on Основное setMenuCursor(true) else setMenuCursor(false) end end ---------------------------------------------------------------------- -- main ---------------------------------------------------------------------- function main() while not isSampAvailable() do wait(100) end ensureConfigDir() loadSettings() loadKladCount() cfg.zoneLeft = tonumber(cfg.zoneLeft) or 0 cfg.zoneUp = tonumber(cfg.zoneUp) or 0 cfg.zoneRight = tonumber(cfg.zoneRight) or 0 cfg.zoneDown = tonumber(cfg.zoneDown) or 0 cfg.zoneSavedAt = tonumber(cfg.zoneSavedAt) or 0 if cfg.zoneSaved == true or cfg.zoneSaved == 'true' then cfg.zoneSaved = true else cfg.zoneSaved = false end loadDopki() wait(800) initImgui() -- 1) сначала онлайн-проверка лицензии (бан / срок), потом точки local licenseGateDone = false lua_thread.create(function() wait(400) local key = tostring(cfg.licenseKey or '') if key ~= '' then local vok, vmsg = validateLicenseOnline() notify('boot license: ' .. tostring(vok) .. ' ' .. tostring(vmsg)) if not vok and vmsg ~= 'no_response' and vmsg ~= 'busy' then stripAccessVisuals() end end -- 2) грузим точки только если подписка ещё активна локально if licenseIsActive() then local n, err = loadCoords() if err then notify('coords: ' .. tostring(err)) else notify(T('Точек загружено: ') .. tostring(n)) end else stripAccessVisuals() notify('coords skipped: no active license') end pcall(checkUpdates) licenseGateDone = true end) do local flagPath = join(CONFIG_DIR, 'gk_reopen.flag') local flag = readFile(flagPath) local okPath = join(CONFIG_DIR, 'gk_update_ok.flag') local okVer = readFile(okPath) if okVer and okVer ~= '' then pcall(os.remove, okPath) updateFooterNotice = { kind = 'ok', text = 'Обновление v' .. tostring(okVer):gsub('%s+', '') .. ' установлено', expiresAt = os.time() + 45, } chatMsg('Обновление успешно: v' .. tostring(okVer):gsub('%s+', '')) end if flag and flag ~= '' then pcall(os.remove, flagPath) pcall(os.remove, flagPath) lua_thread.create(function() local tries = 0 while tries < 50 and not imguiReady do wait(100); tries = tries + 1 end wait(200) if menuOpen then menuOpen[0] = true activeTab = 1 setMenuCursor(true) end end) end end lua_thread.create(function() local tries = 0 while tries < 200 and not isSpawned() do wait(100); tries = tries + 1 end -- ждём проверку лицензии (до ~15 сек) local w = 0 while not licenseGateDone and w < 150 do wait(100); w = w + 1 end wait(200) if not licenseIsActive() then stripAccessVisuals() notify('blips skipped: license inactive') return end local nb = refreshBlips() or 0 local n3 = refreshMarkers3d() or 0 refreshDopBlips() notify(string.format('blips=%s 3d=%s points=%s', tostring(nb), tostring(n3), tostring(#mainPoints))) end) sampRegisterChatCommand('gk', toggleMenu) sampRegisterChatCommand('gkkey', function(arg) arg = tostring(arg or ''):gsub('^%s+', ''):gsub('%s+$', '') if arg == '' then chatMsg('Использование: /gkkey ВАШ-КЛЮЧ') return end lua_thread.create(function() local ok, info = activateLicense(arg) if not ok then chatMsg('Активация: ' .. tostring(info)) end if ui and ui.licenseKey then setCharBuf(ui.licenseKey, arg, 128) end end) end) sampRegisterChatCommand('gkupdate', function() lua_thread.create(function() pcall(checkUpdates) local ok, n = downloadCoordsFromServer() if ok then chatMsg('Coords: ' .. tostring(n)) else chatMsg('Update: ' .. tostring(n)) end end) end) local function zoneOpsOk() if not cfg.zoneOps then chatMsg('Операции с зоной выключены.') return false end return true end local function zonedel_func() if not zoneOpsOk() then return end clearKladZone(false) if cfg.pointcheck then refreshBlips() end chatMsg('Зона удалена.') end sampRegisterChatCommand('zonedel', zonedel_func) sampRegisterChatCommand('zd', zonedel_func) local function zonecopy_func() if not zoneOpsOk() then return end if not zoneActive or not zone then chatMsg('Нет активной зоны.') return end local text = string.format('/zonepaste l: %.4f; u: %.4f; r: %.4f; d: %.4f', zone.left, zone.up, zone.right, zone.down) if type(setClipboardText) == 'function' then pcall(setClipboardText, text) end print('[Golden Klad] ' .. text) chatMsg('Зона скопирована.') end sampRegisterChatCommand('zonecopy', zonecopy_func) sampRegisterChatCommand('zc', zonecopy_func) local function zonepaste_func(arg) if not zoneOpsOk() then return end arg = tostring(arg or '') local clip = arg if clip == '' and type(getClipboardText) == 'function' then local ok, c = pcall(getClipboardText) if ok and c then clip = tostring(c) end end local l = tonumber(clip:match('[Ll]:%s*([%-%d%.]+)')) local u = tonumber(clip:match('[Uu]:%s*([%-%d%.]+)')) local r = tonumber(clip:match('[Rr]:%s*([%-%d%.]+)')) local d = tonumber(clip:match('[Dd]:%s*([%-%d%.]+)')) if not (l and u and r and d) then chatMsg('Формат: /zp l:..; u:..; r:..; d:..') return end setKladZone(l, u, r, d) if cfg.pointcheck then refreshBlips() end chatMsg('Зона вставлена.') end sampRegisterChatCommand('zonepaste', zonepaste_func) sampRegisterChatCommand('zp', zonepaste_func) sampRegisterChatCommand('trreload', function() local n = select(1, loadCoords()) notify(string.format('points=%s blips=%s 3d=%s', n, refreshBlips() or 0, refreshMarkers3d() or 0)) end) sampRegisterChatCommand('trdop', function() notify(addDopHere() and T('Доп. Клад +') or T('ошибка')) end) sampRegisterChatCommand('trfix', function() notify(string.format('spawn=%s sprite=%s 3d=%s line=%s', tostring(isSpawned()), tostring(type(addSpriteBlipForCoord) == 'function'), tostring(type(createUser3dMarker) == 'function'), tostring(type(renderDrawLine) == 'function'))) end) local lastBlip, last3d, lastCheck, lastDopExpire, wasSpawned = 0, 0, 0, 0, false while true do wait(0) local now = os.clock() local spawned = isSpawned() if spawned and not wasSpawned then -- after reconnect/spawn: restore zone then blips zoneNeedsRestore = true zoneRestoreAt = now + 1.5 refreshBlips(); refreshMarkers3d(); refreshDopBlips() end wasSpawned = spawned if zoneNeedsRestore and now >= (tonumber(zoneRestoreAt) or 0) then zoneNeedsRestore = false if restoreZoneFromCfg() then if cfg.pointcheck then pcall(refreshBlips) end if cfg.markers3d then pcall(refreshMarkers3d) end end end -- ground circles every frame (cheap, only on-screen) pcall(renderGroundCircles) pcall(renderCounterHud) if cfg.dopki and now - (lastDopExpire or 0) > 30 then lastDopExpire = now pcall(removeExpiredDops, true) end -- if user closed window with X, release cursor if menuCursorOwned and (not imguiReady or not menuOpen or not menuOpen[0]) then setMenuCursor(false) end -- dig cursor must not run while menu is open (steals/hides cursor) if not (menuOpen and menuOpen[0]) then digCursorTick(now) end if cfg.scriptEnabled and cfg.pointcheck and spawned and now - lastCheck > 0.15 then lastCheck = now; pointcheckTick() end if cfg.scriptEnabled and cfg.pointcheck and spawned and now - lastBlip > 2.0 then lastBlip = now; refreshBlips() end if cfg.scriptEnabled and cfg.markers3d and cfg.showArrows and spawned and now - last3d > 2.0 then last3d = now; refreshMarkers3d() end end end function onScriptTerminate(script, quit) if thisScript and script == thisScript() then pcall(stopDigCursorLock) pcall(setMenuCursor, false) pcall(clearDopNavigator) clearMainBlips(); clearMarkers3d(); clearDopBlips() pcall(removeGangZone, zoneVisualId) end end