init.lua591 lines
---@diagnostic disable: undefined-global
--- Screen presets: "minimum / usual / everything" for what evolve draws on screen, one call to
--- apply, one to undo, and the count of values each would change. Plus the A-Z catalogue of every
--- menu group by category. Public `ui.*` only: a preset writes the same switches a player clicks.
---
--- A switch is a group's main checkbox ("Enable", "0. Enabled {{utea}}"); a group without one
--- gives its own top-level checkboxes. "usual" puts every switch back to its default, "all"
--- turns every switch on, "minimum" keeps only `KEEP` on. No saved key moves; the only new keys
--- are the rows `install` makes in its own group.
---
--- ```lua
--- local P = require("lib/screen_presets")
--- local n = P.changes("minimum") -- values it would write
--- P.apply("minimum") -- writes them, remembers the old ones
--- P.undo() -- puts them back
--- for _, L in ipairs(P.catalog("ru")) do print(L.letter, #L.items) end
--- ```
-- #535 R2 step 5. the groups and captions: data/categories.lua, generated from
-- tools/menu535/categories.json by tools/menu535/gen_presets_data.py
local DATA = require("lib/screen_presets/data/categories")
local M = {}
M.PRESETS = { "minimum", "usual", "all" }
-- the sections of "на экране" a preset covers (categories.json cat "screen"); the camera
-- section is not drawn info, a preset leaves it alone
M.SECTIONS = {
{ id = "over", ru = "Над героями", en = "Over heroes" },
{ id = "map", ru = "Карта", en = "Map" },
{ id = "timers", ru = "Таймеры", en = "Timers" },
{ id = "panels", ru = "Панели", en = "Panels" },
}
local SEC_ID = {}
for _, s in ipairs(M.SECTIONS) do SEC_ID[s.ru] = s.id end
local CAMERA = "Камера"
-- what "minimum" keeps on: hp bars (basic readability), map alerts, minimap marks, wards,
-- rune and roshan timers. everything else drawn goes off
M.KEEP = {
["Visuals.Visualization - HP bar"] = true,
["Combo.Alerts"] = true,
["Combo.Minimap"] = true,
["Combo.Ward"] = true,
["Combo.Rune Timers"] = true,
["Combo.Roshan Tracker"] = true,
}
-- the groups install() made: the preset's own rows, not a feature for the catalogue
local own = {}
local CAT_EN = {
home = "Home", hero = "My hero", fight = "Fight and items", lane = "Lane and farm",
screen = "Screen and map", draft = "Draft and players", humanizer = "Humanizer",
keys = "Keys", store = "Store", settings = "Settings", other = "Other",
}
local CAT_RU = { other = "Другое" }
for _, r in ipairs(DATA.rail) do CAT_RU[r.id] = r.ru end
local function split(key)
local dot = key:find(".", 1, true)
if not dot then return key, "general" end
return key:sub(1, dot - 1), key:sub(dot + 1)
end
M.split = split
local function lang_of(lang)
if lang then return lang end
local ok, l = pcall(function() return ui.menu_lang() end)
return ok and l == "ru" and "ru" or "en"
end
-- ------------------------------------------------------------------ groups --
-- groups a third-party script put under the presets: { key =, sec =, keep = }
local added = {}
--- Put a group of your own under the presets ("tab.group"). opts.keep = true keeps it on in "minimum".
---@param key string
---@param sec string|nil over|map|timers|panels (default panels)
---@param opts table|nil keep
function M.add_group(key, sec, opts)
assert(type(key) == "string" and key:find(".", 1, true), "screen_presets.add_group: key is \"tab.group\"")
for _, g in ipairs(added) do
if g.key == key then g.sec = sec or g.sec; g.keep = opts and opts.keep or false; return end
end
added[#added + 1] = { key = key, sec = sec or "panels", keep = opts and opts.keep or false }
end
local DATA_BY = {}
for _, g in ipairs(DATA.groups) do DATA_BY[g.key] = g end
-- the rail as the menu lists it (#535 R2 step 2): { [category id] = { { key = "tab.group",
-- caption = }, ... } }, "other" for "@*". nil on a menu without category views (an older dll,
-- the offline mock), and the category map stands in
local function live_views()
local ok, lay = pcall(function() return ui.menu_layout() end)
if not ok or type(lay) ~= "table" then return nil end
local out, any = {}, false
for _, v in ipairs(lay) do
local name = type(v) == "table" and v.name
if type(name) == "string" and name:sub(1, 1) == "@" and type(v.groups) == "table" then
local id = name:sub(2)
if id == "*" then id = "other" end
local list = out[id] or {}
for _, g in ipairs(v.groups) do
local t, gr = tostring(g.key):match("^([^\31]*)\31(.*)$")
if t then list[#list + 1] = { key = t .. "." .. gr, caption = g.caption } end
end
out[id] = list
any = true
end
end
if not any then return nil end
-- the live layout lists a page's sub-tabs as entries of their own ("Combo.Alerts.Rune spawns",
-- caption "Оповещения на карте / Появление рун"): fold them into the page they extend
for id, list in pairs(out) do
local keys = {}
for _, g in ipairs(list) do keys[g.key] = g end
local folded = {}
for _, g in ipairs(list) do
local p, head = g.key:match("^(.*)%.[^.]*$"), nil
while p do
if keys[p] then head = keys[p] end
p = p:match("^(.*)%.[^.]*$")
end
if head then head.subs = true else folded[#folded + 1] = g end
end
for _, g in ipairs(folded) do
if g.subs and type(g.caption) == "string" then g.caption = g.caption:match("^(.-) / [^/]*$") or g.caption end
g.subs = nil
end
out[id] = folded
end
return out
end
M._live_views = live_views
--- Every group a preset covers, in menu order: the groups the menu lists under "screen" (their
--- own ui.group_meta category, or their script's), without the camera section.
---@return table[] { key, sec, keep }
function M.groups()
local out, seen = {}, {}
local views = live_views()
if views then
for _, g in ipairs(views.screen or {}) do
local d = DATA_BY[g.key]
-- a group the map does not section (a third-party script's) still counts
local sec
if not (d and d.sec == CAMERA) then sec = d and SEC_ID[d.sec] or "other" end
if sec and not own[g.key] and not seen[g.key] then
out[#out + 1] = { key = g.key, sec = sec, keep = M.KEEP[g.key] == true }
seen[g.key] = true
end
end
else
for _, g in ipairs(DATA.groups) do
-- a merged group's parts hang under its head's switch
if g.cat == "screen" and SEC_ID[g.sec] and (not g.merge or g.merge == g.key) then
out[#out + 1] = { key = g.key, sec = SEC_ID[g.sec], keep = M.KEEP[g.key] == true }
seen[g.key] = true
end
end
end
for _, g in ipairs(added) do
if not seen[g.key] then out[#out + 1] = { key = g.key, sec = g.sec, keep = g.keep } end
end
return out
end
-- ---------------------------------------------------------------- switches --
local function is_master(label)
local l = tostring(label):lower():gsub("%s*{{%w+}}%s*$", ""):gsub("^%d+%.%s*", "")
return l == "enable" or l == "enabled"
end
M.is_master = is_master
local function on(c) local v = c:get(); return v == true or v == 1 end
--- The switches as they are now; a group whose script is off gives none.
---@return table[] { key, ctl, group, sec, keep }
function M.switches()
local out = {}
for _, g in ipairs(M.groups()) do
local tab, group = split(g.key)
local ok, ctls = pcall(ui.group_controls, tab, group)
if ok and type(ctls) == "table" then
local boxes, master = {}, nil
for _, c in ipairs(ctls) do
if c:type() == "checkbox" then
boxes[#boxes + 1] = c
if not master and is_master(c:label()) then master = c end
end
end
for _, c in ipairs(master and { master } or boxes) do
out[#out + 1] = { key = g.key .. "." .. c:label(), ctl = c, group = g.key,
sec = g.sec, keep = g.keep }
end
end
end
return out
end
-- what preset `name` wants from a switch: true/false, or nil = its default
local function wants(name, s)
if name == "minimum" then return s.keep end
if name == "all" then return true end
if name == "usual" then return nil end
error("screen_presets: unknown preset " .. tostring(name), 3)
end
--- What applying a preset would write.
---@param name string minimum|usual|all
---@param list table[]|nil switches(), when the caller has them
---@return integer n
---@return table[] switches
function M.changes(name, list)
list = list or M.switches()
local out = {}
for _, s in ipairs(list) do
local w = wants(name, s)
if w == nil then
if not s.ctl:is_default() then out[#out + 1] = s end
elseif on(s.ctl) ~= w then
out[#out + 1] = s
end
end
return #out, out
end
--- The preset the switches match now, nil for the player's own mix.
---@param list table[]|nil
---@return string|nil
function M.current(list)
list = list or M.switches()
if #list == 0 then return nil end
for _, p in ipairs({ "usual", "minimum", "all" }) do
if M.changes(p, list) == 0 then return p end
end
return nil
end
-- ------------------------------------------------------------------- undo --
-- the values the last apply overwrote: { { key =, value = bool }, ... }. on_undo(text) is
-- install's hook that keeps them in its hidden textbox, so an undo survives a restart
local undo_list, on_undo = {}, nil
--- The undo as text, one "1<tab>key" line per value.
---@param list table[]|nil
---@return string
function M.encode_undo(list)
local t = {}
for _, u in ipairs(list or undo_list) do t[#t + 1] = (u.value and "1" or "0") .. "\t" .. u.key end
return table.concat(t, "\n")
end
--- Replace the undo with one read back from encode_undo's text.
---@param text string
---@return integer values held
function M.load_undo(text)
undo_list = {}
for line in tostring(text or ""):gmatch("[^\n]+") do
local v, k = line:match("^([01])\t(.+)$")
if k then undo_list[#undo_list + 1] = { key = k, value = v == "1" } end
end
return #undo_list
end
--- How many values the undo would put back.
---@return integer
function M.can_undo() return #undo_list end
local function remembered(list)
undo_list = list
if on_undo then pcall(on_undo, M.encode_undo()) end
end
--- Apply a preset. A preset that changes nothing keeps the last undo.
---@param name string minimum|usual|all
---@return integer values written
function M.apply(name)
local list = M.switches()
local n, ch = M.changes(name, list)
if n == 0 then return 0 end -- nothing to write: the last undo stays
local prev = {}
for _, s in ipairs(ch) do
prev[#prev + 1] = { key = s.key, value = on(s.ctl) }
local w = wants(name, s)
if w == nil then s.ctl:reset() else s.ctl:set(w) end
end
remembered(prev)
return n
end
--- Put back the values the last apply overwrote.
---@return integer values restored
function M.undo()
if #undo_list == 0 then return 0 end
local by = {}
for _, s in ipairs(M.switches()) do by[s.key] = s end
local n = 0
for _, u in ipairs(undo_list) do
local s = by[u.key]
-- a switch whose script went away since: nothing to put back
if s then
if on(s.ctl) ~= u.value then s.ctl:set(u.value) end
n = n + 1
end
end
remembered({})
return n
end
-- --------------------------------------------------------------- catalogue --
local function cp_upper(cp)
if cp >= 0x430 and cp <= 0x44F then return cp - 0x20 end
if cp == 0x451 then return 0x401 end
if cp >= 0x61 and cp <= 0x7A then return cp - 0x20 end
return cp
end
-- cyrillic first (ё right after е), then latin, then digits, then the rest
local function rank(cp)
cp = cp_upper(cp)
if cp == 0x401 then return 1000 + (0x415 - 0x410) * 2 + 1 end
if cp >= 0x410 and cp <= 0x42F then return 1000 + (cp - 0x410) * 2 end
if cp >= 0x41 and cp <= 0x5A then return 2000 + cp end
if cp >= 0x30 and cp <= 0x39 then return 3000 + cp end
return 4000 + cp
end
-- the dll's lua has no utf8 library (live, 2026-11-03): decode by hand. a broken byte counts as itself
local function codepoints(s)
local out, i, n = {}, 1, #s
while i <= n do
local c = s:byte(i)
local len = c >= 0xF0 and 4 or c >= 0xE0 and 3 or c >= 0xC0 and 2 or 1
local cp = len == 1 and c or (c & (0x3F >> (len - 1)))
for j = 1, len - 1 do cp = (cp << 6) | ((s:byte(i + j) or 0x80) & 0x3F) end
out[#out + 1] = cp
i = i + len
end
return out
end
local function utf8_char(cp)
if cp < 0x80 then return string.char(cp) end
if cp < 0x800 then return string.char(0xC0 | (cp >> 6), 0x80 | (cp & 0x3F)) end
if cp < 0x10000 then return string.char(0xE0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F)) end
return string.char(0xF0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3F), 0x80 | ((cp >> 6) & 0x3F), 0x80 | (cp & 0x3F))
end
local function sort_key(s)
local k = {}
for _, cp in ipairs(codepoints(s)) do k[#k + 1] = rank(cp) end
return k
end
local function before(a, b)
for i = 1, math.max(#a, #b) do
local x, y = a[i], b[i]
if x == nil then return true end
if y == nil then return false end
if x ~= y then return x < y end
end
return false
end
local function first_letter(s)
local cp = codepoints(s)[1]
return cp and utf8_char(cp_upper(cp)) or "#"
end
local function pretty(group)
local leaf = group:gsub("^[Vv]isuali[sz]ation %- ", ""):gsub("^visuals %- ", "")
return (leaf:gsub("^%l", string.upper))
end
-- the captions the column prints now, by "tab.group" (ui.menu_layout keys are "tab\31group")
local function layout_captions()
local out = {}
local ok, lay = pcall(function() return ui.menu_layout() end)
if not ok or type(lay) ~= "table" then return out end
for _, view in ipairs(lay) do
for _, g in ipairs(type(view) == "table" and type(view.groups) == "table" and view.groups or {}) do
if type(g.key) == "string" and type(g.caption) == "string" and g.caption ~= "" then
local t, gr = g.key:match("^([^\31]*)\31(.*)$")
if t then out[t .. "." .. gr] = g.caption end
end
end
end
return out
end
local function present(key)
local tab, group = split(key)
local ok, ctls = pcall(ui.group_controls, tab, group, true)
return ok and type(ctls) == "table" and #ctls > 0
end
--- Every group a player can open, A-Я then A-Z, grouped by first letter. Read from the menu's own
--- category lists (ui.menu_layout, captions as the column prints them, ui.categories titles); on a
--- menu without them, from the category map with captions in `lang`. opts.dev also lists the
--- developer groups (map only: the menu already shows them in a developer session); opts.other =
--- false drops the groups with no category.
---@param lang string|nil ru|en (default: the menu's)
---@param opts table|nil dev, other
---@return table[] { letter, items = { key, tab, group, caption, cat, cat_caption, sec } }
function M.catalog(lang, opts)
lang, opts = lang_of(lang), opts or {}
local caps, items, seen, known = layout_captions(), {}, {}, {}
local by = {}
for _, g in ipairs(DATA.groups) do by[g.key] = g; known[g.key] = true end
local function caption(g, key)
if lang == "ru" and g and g.caption then return g.caption end
if lang == "ru" and caps[key] then return caps[key] end
local _, group = split(key)
return pretty(group)
end
local function add(key, g, cat)
local tab, group = split(key)
items[#items + 1] = {
key = key, tab = tab, group = group, caption = caption(g, key), cat = cat,
cat_caption = (lang == "ru" and CAT_RU or CAT_EN)[cat] or cat, sec = g and g.sec or "",
}
end
local views = live_views()
if views then
-- the menu's own lists: captions in its language, developer groups already left out of a
-- player's session, a group under the category its script gave it
local titles = {}
pcall(function() for _, c in ipairs(ui.categories()) do titles[c.id] = c.title end end)
for cat, list in pairs(views) do
if cat ~= "settings" and (opts.other ~= false or cat ~= "other") then
for _, g in ipairs(list) do
local tab, group = split(g.key)
local hero_page = group:match("^Heroes%.") and not by[g.key]
if not own[g.key] and not seen[g.key] and not hero_page then
seen[g.key] = true
local d = by[g.key]
items[#items + 1] = {
key = g.key, tab = tab, group = group,
caption = (type(g.caption) == "string" and g.caption ~= "") and g.caption or caption(d, g.key),
cat = cat, cat_caption = titles[cat] or (lang == "ru" and CAT_RU or CAT_EN)[cat] or cat,
sec = d and d.sec or "",
}
end
end
end
end
end
for _, g in ipairs(views and {} or DATA.groups) do
local head = g.merge or g.key
if not seen[head] and g.cat ~= "settings" and (opts.dev or not g.dev) and present(head) then
seen[head] = true
add(head, by[head] or g, g.cat)
end
end
if not views and opts.other ~= false and ui.all_controls then
local ok, all = pcall(ui.all_controls)
for _, c in ipairs(ok and type(all) == "table" and all or {}) do
local tab, group = c:tab(), c:group()
local low = tab:lower()
local key = tab .. "." .. group:match("^[^.]*")
-- a group the map files already (its own key or one it extends), the hero list,
-- the built-in settings tab
local covered = low == "config" or low == "settings" or group:match("^Heroes%.")
or group == "Heroes"
if not covered then
local p = tab .. "." .. group
while p do
if known[p] then covered = true; break end
p = p:match("^(.*)%.[^.]*$")
end
end
if not covered and not seen[key] and not own[key] then
seen[key] = true
add(key, nil, "other")
end
end
end
for _, it in ipairs(items) do it._k = sort_key(it.caption) end
table.sort(items, function(a, b)
if before(a._k, b._k) then return true end
if before(b._k, a._k) then return false end
return a.key < b.key
end)
local out, last = {}, nil
for _, it in ipairs(items) do
it._k = nil
local l = first_letter(it.caption)
if not last or last.letter ~= l then last = { letter = l, items = {} }; out[#out + 1] = last end
last.items[#last.items + 1] = it
end
return out
end
-- -------------------------------------------------------------- menu rows --
local TEXT = {
ru = { title = "Пресет экрана", names = { "Минимум", "Обычный", "Всё", "Свой" },
hint = "Изменит значений: минимум %d, обычный %d, всё %d",
undo = "Отменить пресет (вернуть %d)",
tip = "Минимум: над героями только полоски HP, на карте оповещения, миникарта и варды, "
.. "таймеры рун и Рошана. Обычный: как после установки. Всё: включено всё, что рисует evolve." },
en = { title = "Screen preset", names = { "Minimum", "Usual", "Everything", "Custom" },
hint = "Values it changes: minimum %d, usual %d, everything %d",
undo = "Undo preset (restore %d)",
tip = "Minimum: only hp bars over heroes; map alerts, minimap marks and wards; rune and "
.. "Roshan timers. Usual: as installed. Everything: all evolve draws is on." },
}
local CUSTOM = 3
--- The menu rows in opts.group (default "Misc.screen presets"): segments showing the matching
--- preset ("custom" when none) that apply one on a click, an undo button, the counts in the hint.
--- Call at file scope, then handle:update() from on_update: it notices a click on the next call and
--- recounts once a second.
---@param opts table|nil group, lang
---@return table handle with update(), counts, seg, undo_btn, store
function M.install(opts)
opts = opts or {}
local G = opts.group or "Misc.screen presets"
local tab, group = split(G)
local L = TEXT[lang_of(opts.lang)]
local H = { group = G }
own[G] = true
local seg = ui.slider_int(G, "Screen preset", 0, CUSTOM, 1)
local undo_btn, store
pcall(function() seg:names(L.names) end)
pcall(function() seg:caption(L.title); seg:tooltip(L.tip) end)
store = ui.textbox(G, "undo", "")
pcall(function() store:visible(false) end)
undo_btn = ui.button(G, "Undo preset", function() M.undo(); H.next = 0; H:update() end)
pcall(function() undo_btn:visible(false) end)
-- home_head: the R2 home draws these rows in the head of the screen column, not as a tile
pcall(ui.group_meta, tab, group, { caption = L.title, icon = "layers", category = "screen", home_head = true })
H.seg, H.undo_btn, H.store = seg, undo_btn, store
on_undo = function(text) store:set(text) end
if M.can_undo() == 0 then M.load_undo(store:get()) end
-- the segment value as last written by lua or a config load. the menu's widgets write the
-- control directly and fire no on_change (ui2_rows.cpp, render thread), a config load and
-- c:set do: so on_change is state to follow, and a value that moved without one is a click
local shown
seg:on_change(function() shown = seg:get() end)
-- every frame: a click on a segment applies at once. the recount runs once a second
function H:update(now)
now = now or (globals and globals.realtime and globals.realtime()) or 0
local v = seg:get()
if shown ~= nil and v ~= shown then
shown = v
local name = M.PRESETS[(tonumber(v) or -1) + 1]
if name then M.apply(name) end
self.next = 0
end
local okm, open = pcall(ui.menu_open)
local closed = okm and open == false
if self.closed and not closed then self.next = 0 end
self.closed = closed
if self.next and now < self.next then return end
self.next = now + 1.0
-- a reload before the first update: the textbox holds the undo, not the module
if shown == nil and M.can_undo() == 0 then M.load_undo(store:get()) end
-- menu_layout + group_controls is a full menu pass, 4.75 ms (#624). closed menu: the list
-- is kept until the control count moves or 30 s pass, the segment still follows the switches
-- (a stale one was written on the next open, the api gate saw it as a call's write)
local nc = 0
pcall(function() nc = ui.control_count and ui.control_count() or 0 end)
if not closed or not self.list or self.list_n ~= nc or now >= (self.list_due or 0) then
self.list, self.list_n, self.list_due = M.switches(), nc, now + 30
end
local list = self.list
local cur = M.current(list)
v = CUSTOM
for i, p in ipairs(M.PRESETS) do if p == cur then v = i - 1 end end
if seg:get() ~= v then seg:set(v) end
shown = v
if store:get() ~= M.encode_undo() then store:set(M.encode_undo()) end
if closed then return end
local n1, n2, n3 = M.changes("minimum", list), M.changes("usual", list), M.changes("all", list)
self.counts = { minimum = n1, usual = n2, all = n3 }
pcall(function() seg:hint(L.hint:format(n1, n2, n3)) end)
local u = M.can_undo()
pcall(function() undo_btn:visible(u > 0); undo_btn:caption(L.undo:format(u)) end)
end
return H
end
return M
data/categories.lua86 lines
-- generated by tools/menu535/gen_presets_data.py from tools/menu535/categories.json.
-- do not edit: change the json and rerun. order = the json's.
return {
rail = {
{ id = "home", ru = "Главная", builtin = true },
{ id = "hero", ru = "Мой герой" },
{ id = "fight", ru = "Бой и предметы" },
{ id = "lane", ru = "Линия и фарм" },
{ id = "screen", ru = "Экран и карта" },
{ id = "draft", ru = "Драфт и игроки" },
{ id = "humanizer", ru = "Хуманайзер" },
{ id = "keys", ru = "Клавиши", builtin = true },
{ id = "store", ru = "Магазин", builtin = true },
{ id = "settings", ru = "Настройки", builtin = true },
},
groups = {
{ key = "Combo.uTEA", cat = "hero", sec = "", caption = "Для всех героев" },
{ key = "Combo.Misc", cat = "hero", sec = "", caption = "Аим" },
{ key = "Combo.Heroes.Custom games", cat = "hero", sec = "", caption = "Комбо в кастомках" },
{ key = "Combo.Item Manager", cat = "fight", sec = "Предметы" },
{ key = "Combo.Item Advisor", cat = "fight", sec = "Предметы" },
{ key = "Combo.Armlet Abuse", cat = "fight", sec = "Предметы" },
{ key = "Combo.Dispel", cat = "fight", sec = "Предметы", caption = "Снятие дебаффов" },
{ key = "Combo.Power Treads", cat = "fight", sec = "Предметы", caption = "Power Treads" },
{ key = "Combo.Orbwalker", cat = "fight", sec = "Бой" },
{ key = "Combo.Smart Dodge", cat = "fight", sec = "Бой" },
{ key = "Combo.Auto Interrupt", cat = "fight", sec = "Бой", caption = "Авто прерывание" },
{ key = "Combo.Kill Steal", cat = "fight", sec = "Бой", caption = "Добить спеллом" },
{ key = "Combo.Defend", cat = "fight", sec = "Бой" },
{ key = "Combo.Illusion", cat = "fight", sec = "Бой", caption = "Управление иллюзиями" },
{ key = "Combo.Last Hit", cat = "lane", sec = "" },
{ key = "Combo.Creep Blocker", cat = "lane", sec = "" },
{ key = "Combo.Lane Control", cat = "lane", sec = "" },
{ key = "Combo.Jungle", cat = "lane", sec = "" },
{ key = "Combo.Camp Stacker", cat = "lane", sec = "" },
{ key = "Combo.Dominion", cat = "lane", sec = "" },
{ key = "Combo.Autopilot", cat = "lane", sec = "" },
{ key = "Combo.Snatcher", cat = "lane", sec = "", caption = "Подбор рун" },
{ key = "Visuals.Visualization - HP bar", cat = "screen", sec = "Над героями", caption = "Полоски HP" },
{ key = "Visuals.Visualization - Status effects", cat = "screen", sec = "Над героями" },
{ key = "Visuals.Visualization - Panels", cat = "screen", sec = "Над героями", caption = "Способности, предметы" },
{ key = "Visuals.visuals - esp", cat = "screen", sec = "Над героями", caption = "ESP: рамки и имена" },
{ key = "Visuals.Visualization - Offscreen ESP", cat = "screen", sec = "Над героями" },
{ key = "Combo.Illusion Marker", cat = "screen", sec = "Над героями" },
{ key = "Combo.Blink Range", cat = "screen", sec = "Над героями" },
{ key = "Combo.uVBE", cat = "screen", sec = "Над героями", caption = "SEEN: тебя видят враги" },
{ key = "Combo.Alerts", cat = "screen", sec = "Карта", caption = "Оповещения на карте" },
{ key = "Combo.Minimap", cat = "screen", sec = "Карта" },
{ key = "Combo.Awareness", cat = "screen", sec = "Карта", caption = "Способности врагов" },
{ key = "Combo.Ward", cat = "screen", sec = "Карта", caption = "Варды" },
{ key = "Combo.Ward Spots", cat = "screen", sec = "Карта" },
{ key = "Combo.Enemy Couriers", cat = "screen", sec = "Карта" },
{ key = "Combo.Rune Timers", cat = "screen", sec = "Таймеры" },
{ key = "Combo.Roshan Tracker", cat = "screen", sec = "Таймеры" },
{ key = "Combo.Lotus Pool Timer", cat = "screen", sec = "Таймеры" },
{ key = "Visuals.Visualization - Top bars", cat = "screen", sec = "Панели" },
{ key = "Visuals.Visualization - Stats panel", cat = "screen", sec = "Панели" },
{ key = "Combo.Net Worth", cat = "screen", sec = "Панели", caption = "Нетворс" },
{ key = "Combo.HUD", cat = "screen", sec = "Панели", caption = "Строка статуса uTEA" },
{ key = "Misc.advisor", cat = "screen", sec = "Панели", caption = "Советник по игре", merge = "Misc.advisor", sub = "Основное" },
{ key = "Misc.advisor.minimap", cat = "screen", sec = "Панели", merge = "Misc.advisor", sub = "Миникарта" },
{ key = "Misc.advisor.allies", cat = "screen", sec = "Панели", merge = "Misc.advisor", sub = "Союзники" },
{ key = "Misc.advisor.coach", cat = "screen", sec = "Панели", merge = "Misc.advisor", sub = "Тренер" },
{ key = "Misc.advisor.classes", cat = "screen", sec = "Панели", merge = "Misc.advisor", sub = "Какие советы" },
{ key = "Misc.advisor.phrases", cat = "screen", sec = "Панели", merge = "Misc.advisor", sub = "Фразы в чат" },
{ key = "Misc.Camera", cat = "screen", sec = "Камера", caption = "Камера" },
{ key = "Visuals.Visualization - Weather", cat = "screen", sec = "Камера" },
{ key = "Visuals.Visualization", cat = "screen", sec = "Камера", caption = "Визуализация (весь скрипт)" },
{ key = "Misc.draft advisor", cat = "draft", sec = "", caption = "Подсказки на драфте" },
{ key = "Combo.Auto Pick", cat = "draft", sec = "" },
{ key = "Misc.Draft ranks", cat = "draft", sec = "", caption = "Ранги на драфте" },
{ key = "Misc.player notes", cat = "draft", sec = "", caption = "Заметки об игроках" },
{ key = "Misc.session", cat = "draft", sec = "", caption = "Итоги матча" },
{ key = "Misc.Chat translate", cat = "draft", sec = "", caption = "Перевод чата" },
{ key = "Misc.New account", cat = "draft", sec = "", caption = "Новый аккаунт" },
{ key = "Humanizer.General", cat = "humanizer", sec = "", caption = "Основное" },
{ key = "Humanizer.Behavior", cat = "humanizer", sec = "" },
{ key = "Humanizer.Part B", cat = "humanizer", sec = "", dev = true },
{ key = "Humanizer.Debug", cat = "humanizer", sec = "", dev = true },
{ key = "Misc.Theme", cat = "settings", sec = "", merge = "appearance" },
{ key = "Misc.general", cat = "settings", sec = "", merge = "appearance" },
{ key = "Misc.fps mode", cat = "settings", sec = "", merge = "appearance" },
{ key = "Misc..BotTrainer", cat = "settings", sec = "", caption = "Тренер ботов", dev = true },
},
}