# spype Lua API — полный справочник

Полный API скриптов spype. Уровень Fatality/Gamesense/Neverlose: скрипты живут
постоянно, имеют свой `lua_State`, изолированы друг от друга и получают все
события и хуки чита.

- Скрипты лежат в `bin/scripts/*.lua`, включаются галкой в меню → Scripts.
- Верхний уровень выполняется один раз; `on_paint()` вызывается каждый кадр
  (или используйте `events.draw:add(fn)` — то же самое).
- `__shutdown()` вызывается при выгрузке скрипта (НЕ вызывается при ошибке).
- `error("...")` останавливает скрипт, текст виден во вкладке Scripts —
  с позицией и стеком вызовов (`scripts/xxx.lua:42: attempt to index nil 'x'`
  + stack traceback).
- Любая unsafe-операция возвращает `nil`/`false` вместо падения.

Быстрая проверка окружения — запустите `bin/scripts/showcases.lua` (блок 10
пишет в консоль чита, что живое).

Содержание:
0. [Золотая тропа — как писать правильно](#золотая-тропа)
1. [Глобальные функции](#глобальные-функции)
2. [math.*](#math)
3. [vec.* / векторы](#vec-векторы)
4. [game.* — игроки, энтити, трейсы, камера, cmd](#game)
4b. [Безопасность энтити — is_valid/is_alive/entity_by_handle](#безопасность-энтити)
5. [entities.* (Fatality-стиль)](#entities)
6. [view.* / input.set_blocked — камера и захват ввода](#view-inputset_blocked)
7. [events.* — все события](#events)
8. [draw.* — 2D/3D-рендер](#draw)
9. [draw.surface — Layer-API Fatality](#drawsurface-layer-api-fatality)
10. [draw.textures / draw.fonts — Accessor-стиль](#drawtextures-drawfonts-accessor-стиль)
11. [scene.* — 3D-примитивы](#scene)
12. [gui.* — меню и контролы](#gui)
13. [ui.* — процедурные контролы + Gamesense new_*](#ui)
14. [cheat.* — хуки, kv, профиль](#cheat)
15. [rage.* / antiaim.* — управление боем](#rage-antiaim)
16. [memory.* — память процесса](#memory)
17. [schema / interfaces / materials / cvar](#schema-interfaces-materials-cvar)
18. [http.* — сеть](#http)
19. [utils.* — утилиты](#utils)
20. [Autocomplete: spype.d.lua (VSCode)](#autocomplete-spypedlua-vscode)
21. [ws.* — workshop-утилиты](#ws)
22. [sound.* — звуки](#sound)
23. [chat.* / console.*](#chat-console)
24. [binds.* / config.* / recorder.*](#binds-config-recorder)
25. [world.* / hud.*](#world-hud)
26. [particles.* / panorama.*](#particles-panorama-блок-12-реверс-слой)
27. [net.* / voice.*](#net-voice-блок-12-реальные-данные)
28. [Compat-глобалы (Gamesense/Fatality)](#compat-глобалы)
29. [FFI](#ffi)
30. [Песочница](#песочница)
31. [Render contract (стейты, клип, scene)](#render-contract-что-гарантировано-при-рисовании)
32. [UI: три слоя (gui/ui/ui.new_*)](#ui-три-слоя-канон-alias)
33. [script.bus / script.store — межскриптовая шина](#scriptbus-scriptstore)
34. [lagcomp.* — данные отката для скриптов](#lagcomp-данные-отката-для-скриптов)
35. [Rage: поведение селектора (не API)](#rage-поведение-селектора-не-api)
36. [Жизненный цикл скрипта](#жизненный-цикл-скрипта)
37. [Типичные ошибки (симптом → причина → фикс)](#типичные-ошибки)
38. [Fatality-паритет: новое в API (октябрь)](#fatality-паритет-новое-в-api-октябрь)
39. [Movement-ядро: movement.* / prediction / scan data / debug](#movement-ядро)
40. [Ragebot: мультипоинты по хитбоксам](#ragebot-мультипоинты)
41. [Не реализовано (честно)](#не-реализовано-честно)

---

## Золотая тропа

**Как писать правильно (один способ на каждую задачу):**

```lua
-- ДЕЛАЙ ТАК:
local me = game.local_player( )            -- entity-объект (авто-свежесть не гарантирована)
if me.valid and me.alive then
    local head = me:bone( 7 )             -- {x,y,z}
end

for _, p in ipairs( game.players( ) ) do   -- свежий список каждый кадр
    if p.valid and p.is_enemy then ... end  -- is_enemy = враг относительно меня
end
```

**НЕ делай так:**
- не храните `me` в глобале на всю сессию — пеш может умереть/пересоздаться.
  Правильно: либо проверять `me.valid` каждый раз, либо хранить `handle`
  и доставать через `game.entity_by_handle(h)`;
- не пишите цикл по «сырым» таблицам `game.get_entities()` без проверки
  `game.is_alive_ptr(entry.ptr)`;
- не используйте одновременно `game.get_health(ptr)` и `p.health` в одном
  скрипте — выберите один стиль. Оба работают, но мешать — источник багов.

**Три стиля доступа (для тех, кто привык к другому читу):**

| Стиль | Как выглядит | Откуда |
|---|---|---|
| spype-нативный | `game.local_player()`, `p.health`, `p:bone(7)` | наш основной |
| Fatality | `entities.GetLocalPawn()`, `entities.players:for_each(fn)` | compat |
| Gamesense | `entity.get_local_player()` → index, `entity.get_prop(idx, 'm_iHealth')` | compat |

Смешивать можно, но **не обязательно** — всё сводится к одним и тем же данным.

### Карта доступа к данным (какую таблицу для чего)

| Что нужно | Таблица | Канонический вызов |
|---|---|---|
| **Себя** (entity-объект) | `game` | `game.local_player()` → `.health`, `.alive`, `:bone(7)` |
| **Враги** (список entity-объектов) | `game` | `game.players()` → цикл, фильтр `p.is_enemy` |
| **Враги** (Fatality-стиль) | `entities` | `entities.players:for_each(fn)` — fn({entity, handle}) |
| **Себя** (низкоуровневая таблица) | `game` | `game.get_local()` → `{pawn, controller, team, ...}` (числа) |
| **Сущность по указателю** | `game` | `game.is_enemy(ptr)`, `game.get_health(ptr)`, `p = game.player(ptr)` |
| **Сущность по CHandle** | `game` | `game.entity_by_handle(handle)` → entity-объект или nil |
| **Оружие активное** | entity | `me:weapon_entity()` → entity-объект оружия, `:ammo()`, `:weapon_id()` |
| **Кости / скелет** | `game` | `game.get_bone(ptr, 7)` (голова), `game.get_skeleton(ptr)` |
| **Хитбоксы** | entity | `me:hitbox(id)` → `{x,y,z}` центра |
| **Visibility** | `game` | `game.is_visible(from, to)` — трейс, bool |
| **Trace line** | `trace` | `trace.line(from, to, [skip])` → `{fraction, end_pos, hit_entity, clear, ...}` |
| **Trace bullet (автоволл)** | `trace` | `trace.bullet(from, to, [skip])` → `{damage, hitgroup, end_pos, ...}` |
| **Мир (туман, небо, погода)** | `world` | `world.get_fog()`, `world.set_weather("rain")`, `world.set_lighting()` |
| **HUD (крест, scope, velocity)** | `hud` | `hud.get_crosshair()`, `hud.set_crosshair_color({...})` |
| **Память процесса** | `memory` | `memory.read_i32(addr)`, `memory.write_float(addr, v)`, `memory.pattern_scan()` |
| **Схема / нетвары** | `schema` | `schema.offset(class, field)`, `schema.get(ptr, class, field)`, `p.m_iHealth` |
| **Интерфейсы движка** | `interfaces` | `interfaces.get('client:CGameEntitySystem')` |
| **Convars** | `console` | `console.get("sv_gravity")`, `console.exec("echo hi")` |
| **Настройки чита** | `cheat` | `cheat.get_var("ragebot.hit chance")`, `cheat.set_var(name, val)` |
| **Свои UI-контролы** | `ui` | `ui.checkbox("id", "Label", true)`, `ui.get("id")` |
| **Fatality-стиль UI** | `gui` | `gui.ctx:find("rage>aimbot>general")`, `gui.checkbox("id", def)` |
| **2D-рисование** | `draw` | `draw.rect_filled(x, y, w, h, col)`, `draw.text(x, y, "text", col)` |
| **3D-рисование (мир)** | `draw` | `draw.scene.line(a, b, col)`, `draw.scene.hitbox(pawn, 0, col)` |
| **Backtrack данные** | `lagcomp` | `lagcomp.records(pawn)` → `[{tick, origin, bones[27], ...}]` |
| **Управление боем** | `rage` | `rage.get_target()`, `rage.set_hitchance(85)`, `rage.stats()` |
| **Anti-aim** | `antiaim` | `antiaim.get_yaw()`, `antiaim.set_pitch("down")`, `antiaim.override(fn)` |
| **Сеть** | `net` | `net.latency()`, `net.channel()` → integer (CNetChannel адрес) |
| **HTTP** | `http` | `http.Get(url, cb)`, `http.Post(url, data, cb)` |
| **Файлы** | `utils` | `utils.FileRead("test.txt")`, `utils.FileWrite("out.txt", data)` |
| **Lua-модули** | global | `require("lib.vector")` → из `scripts/lib/vector.lua` |
| **Межскриптовая шина** | `script` | `script.bus("name"):emit("event", data)`, `script.store.set(key, v)` |
| **События** | `events` | `events.draw:add(fn)`, `events.create_move:add(fn)`, `events.player_hurt:add(fn)` |
| **Запись движения** | `recorder` | `recorder.start("name")`, `recorder.play({speed=1.0})` |
| **Бинды** | `binds` | `binds.register("peek", 0x45, "hold", fn)` |
| **Пресеты скрипта** | `config` | `config.save("preset")`, `config.load("preset")`, `config.list()` |
| **Звук** | `sound` | `sound.play("hit.wav", 0.5)` → объект `:stop()`, `:is_playing()` |
| **Чат** | `chat` | `chat.say("hello")`, `chat.team_say("gg")` |
| **Консоль** | `console` | `console.exec("cmd")`, `console.get("name")` |
| **Частицы** | `particles` | `particles.create("particles/...vpcf", x, y, z)` |
| **Panorama JS** | `panorama` | `panorama.Eval([[GameInterfaceAPI.ConsoleCommand('...')]])` |
| **FFI (нативные вызовы)** | `ffi` | `ffi.CallVFunc(ptr, 6, 'int(__fastcall*)(void*, float)', 1.5)` |
| **Камера** | `view` | `view.set_origin({x,y,z})`, `view.set_fov(90)` (в override_view) |
| **ESP override** | `esp` | `esp.set_player_override(pawn, {disable=true})` |

**Правило**: если функция из этой карты возвращает `nil` — проверьте контекст
(некоторые работают только в определённых событиях: `view.*` внутри `override_view`,
`game.cmd()` внутри `create_move`, `draw.*` внутри `present_queue`).

---

## Глобальные функции

| Функция | Описание |
|---|---|
| `print(...)` / `log(...)` | вывод в консоль чита (разделитель — таб) |
| `error(text)` | **останавливает скрипт** с ошибкой (текст во вкладке Scripts) |
| `unpack(tbl)` | распаковка таблицы (стандартный LuaJIT) |
| `Delay(sec, fn)` | отложенный вызов через кооперативный планировщик, НЕ блокирует поток |
| `__shutdown()` | определите — вызовется при выгрузке скрипта (не при ошибке) |
| `on_paint()` | определите — вызывается каждый кадр (альтернатива events.draw) |

```lua
print('Hello world!', 123)
Delay(0.5, function() print('через полсекунды') end)
function __shutdown() print('unload ok') end
```

### spype.* — версия контракта и feature detection

| Поле/функция | Что даёт |
|---|---|
| `spype.version` | метка поколения API, сейчас `"2026.10"` |
| `spype.api` | целое: повышается при изменении **семантики** существующих функций (например, `anim_layers`) |
| `spype.features` | таблица `имя = true`: `ffi`, `clipboard`, `files`, `http`, `particles`, `panorama`, `lagcomp`, `net`, `voice`, `recorder`, `script_bus`, `draw_shader` |
| `spype.has(name)` | `bool` — та же таблица, но безопасно для опечаток |

```lua
if spype.api >= 1 and spype.has('lagcomp') then
    local records = lagcomp.records(enemy)   -- новый путь
else
    -- старый fallback
end
```

Правило: `spype.api` читайте до использования функций, чья семантика
менялась; `spype.version` — для логов и отчётов об ошибках.

## math.*

Стандартный Lua math + расширения (все CamelCase — как в Fatality):

| Функция | Описание |
|---|---|
| `math.CalcAngle(src, dst)` | углы `{pitch, yaw, roll=0}` между двумя точками |
| `math.VectorAngles(fwd)` | углы из направления |
| `math.AngleNormalize(a)` | нормализация в [-180; 180] |
| `math.ApproachAngles(from, to, speed)` | приближение углов по скорости |
| `math.EdgePoint(mins, maxs, dir, radius)` | точка на грани бокса в направлении |
| `math.Lerp(a, b, t)` | линейная интерполяция |
| `math.Clamp(n, lo, hi)` | зажим |
| `math.RemapVal(v, a, b, c, d)` / `math.RemapValClamped(...)` | перенос диапазона |
| `math.WorldToScreen(vec)` | `{x, y}` или `nil` (за камерой). Полную форму (`{x, y, screen, w, on_screen}`) даёт `game.world_to_screen` — это ДВЕ разные функции с разным возвратом |
| `math.Vec2(x, y)` / `math.Vec3(x, y, z)` | конструкторы |
| `math.calc_angle/...` | те же в snake_case |

## vec.* / векторы

- `vec.vec3(x, y, z)` — `{x=, y=, z=}` (индексы `[1..3]` тоже работают).

Методы векторных объектов (метатаблица `__spype_vec3_mt`):

| Метод | Возврат |
|---|---|
| `v:len()` / `v:length()` | number (длина) |
| `v:dist(other)` / `v:distance(other)` | number (расстояние между точками) |
| `v:dot(other)` / `v:cross(other)` | number / Vector3 |
| `v:normalized()` / `v:normalize()` | Vector3 (нормализованный) |
| `v:add(w)` / `v:sub(w)` / `v:mul(k)` / `v:div(k)` | Vector3 (арифметика) |
| `v:lerp(w, t)` | Vector3 |
| `v:to_angles()` | Vector3 `{pitch, yaw}` |

```lua
local a = vec.vec3(100, 0, 0)
local d = a:dist(vec.vec3(100, 300, 0))
local mid = a:lerp(vec.vec3(200, 0, 64), 0.5)
```

## game.*

| Функция | Возвращает |
|---|---|
| `game.local_player()` | **entity-объект** (`.valid`, `.alive`, `:bone(7)`, ...) — канон, см. «Золотая тропа» |
| `game.players()` | `{entity-объекты}` свежий список каждый вызов |
| `game.is_enemy(me)` | bool (entity-объект или указатель) |
| `game.get_local()` | низкоуровневая таблица `{pawn, controller, observer_pawn, observer_controller, team, view_team, is_alive, is_valid, view_pawn, view_controller}` — поля с `is_`-префиксом; entity-объекты используют `.valid`/`.alive` без префикса |
| `game.get_entities([type])` | `{{ptr, index, hash, class}, ...}`; type: `"player"/"item"/"projectile"/"all"` |
| `game.get_entity_by_index(idx)` | ptr или nil |
| `game.get_health/get_team/is_alive/get_origin/get_velocity/get_flags/get_name(ptr)` | состояние |
| `game.get_bone(ptr, id)` | `{x,y,z}` (7 = голова) |
| `game.get_skeleton(ptr)` | массив костей |
| `game.get_pose_params(ptr)` / `entity:pose_params()` | `{24 числа}` анимации (m_flPoseParameter) или nil |
| `game.get_hitbox_position(ptr, id)` | `{x,y,z}` центра хитбокса |
| `game.get_bounding_box(ptr)` | `{x, y, w, h}` экранного бокса |
| `game.world_to_screen({x,y,z})` | `{x, y, screen, w, on_screen}` |
| `game.is_visible(from, to, [target], [skip])` | bool |
| `game.trace(from, to, [skip])` | `{hit_entity, fraction, all_solid, position, normal, contents}` |
| `trace.line(from, to, [skip])` / `trace.bullet(from, to, [skip])` (ОТДЕЛЬНАЯ глобальная таблица, не `game.trace.*`) | line: `{fraction, end, end_pos, hit_entity, clear}`; bullet: `{damage, hitgroup, hitgroup_name, fraction, visible, hit_entity, penetrated, end, end_pos}`. **`end` — ключевое слово Lua**: доступ только `r["end"]`; используйте алиас `r.end_pos` |
| `trace.hull(from, to, mins, maxs, [skip], [mask])` | box-sweep: `{fraction, end, end_pos, hit_entity, clear}` — гранаты, «можно ли встать», предсказание движения |
| `trace.sphere(from, to, radius, [skip], [mask])` | sphere-sweep: `{fraction, end, end_pos, hit_entity, clear}` — дешёвый capsule-опрос головы/зазора |
| `game.get_view()` | `{origin, angles, fov, has_camera, matrix[4][4]}` |
| `game.get_view_angles()` / `set_view_angles({p,y,r})` | камера |
| `game.get_active_weapon(ptr)` / `get_ammo` / `is_reloading` / `get_inaccuracy` / `get_spread` / `get_weapon_name` | оружие |
| `game.get_classname(ptr)` / `is_dormant(ptr)` | класс/дормант |
| `game.global_vars` | `{cur_time, tick_count, frame_time, tick_interval, ...}` |
| `game.tick_interval()` / `game.tick_to_time(tick)` / `game.time_to_tick(t)` / `game.time()` / `game.time_since(t)` | часовая база без хардкода 0.015625 и без FFI к global_vars |
| `game.fps()` | текущий FPS рендера (адаптивные визуалы) |
| `game.hitgroup_name(hg)` | `"head"` / `"chest"` / ... или nil |
| `game.netvar_type("m_...")` | тип нетвара по имени: `"int"/"float"/"bool"/"vec3"` |
| `game.netvar_write(pawn, "m_...", value)` | явная запись нетвара через safe-гарды → bool |
| `game.cvar_poll(name)` | cvar-колбэки без C++-пампы: `(value, changed)` — биндинг помнит прошлое значение; зовите из `on_paint`, `changed=true` = значение сменилось с прошлого опроса |
| `game.set_model(pawn, "models/...")` | смена модели сущности (кастом-агенты/визуалы из Lua); SEH-гард → `false` вместо краша |
| `game.engine` | `{in_game, is_connected, screen_size}` |
| `game.PlaySound(path, [vol])` | алиас sound.play |

### cmd (внутри events.createmove)

`game.cmd()` → объект:
`get/set_view_angles`, `get/set_move`, `get/set/add/remove_buttons`,
`set_mouse`, `set_impulse`, `set_weapon_select`, `tick`, `command`, `pitch`, `yaw`;
константы `game.IN_ATTACK`, `IN_JUMP`, `IN_DUCK`, `IN_FORWARD`, `IN_BACK`,
`IN_MOVELEFT`, `IN_MOVERIGHT`, `IN_USE`, `IN_ATTACK2`, `IN_RELOAD`, `IN_SCORE`,
`IN_SPEED`, `IN_WALK`, `IN_ZOOM`.
Субтики: `cmd:subtick_count()`, `cmd:subtick_get(i)` → `{button, pressed, when, ...}`,
`cmd:subtick_add(t, button, pressed)`, `cmd:subtick_clear()`,
**`cmd:subtick_set(i, {button=, pressed=, when=, fwd=, left=})`** — модификация существующего субтика.

Полный доступ к команде (приват-уровень):
`cmd:get_random_seed()` / **`cmd:set_random_seed(n)`** — сид разброса;
`cmd:get_command_tick()` / **`cmd:set_command_tick(n)`** — тик команды.

Freestanding-доступ к cmd без объекта (те же данные, работает и вне cmd-объекта
внутри createmove): `game.cmd_set_view_angles({p,y,r})`, `game.cmd_set_move(x, y)`,
`game.cmd_get_buttons()` → bits, `game.cmd_set_buttons(b)` / `cmd_add_buttons(b)` /
`cmd_remove_buttons(b)`, `game.cmd_set_mouse(dx, dy)`, `game.cmd_set_impulse(i)`,
`game.cmd_set_weapon_select(w)`.

**Когда `game.cmd()` возвращает nil:** вне колбэка `events.create_move`
(или `createmove_post`) cmd не активен — вызов из draw/present/on_paint вернёт
`nil`. Пишите в cmd только из createmove.

## Безопасность энтити

Самая частая причина крашей в скриптах — держать указатель на сущность, жившую
на предыдущем кадре. API защиты:

| Функция | Что делает |
|---|---|
| `game.is_valid(ptr)` | читаемая память по адресу (мгновенно, без вылетов) |
| `game.is_alive_ptr(ptr)` | память читаема + сущность ещё в кэше сущностей + hp > 0 |
| `game.entity_by_handle(h)` | из 32-битного CHandle (index + serial) достать свежий ptr → entity-объект или nil |
| `p.valid` / `p:refetch()` / `p:is_valid()` | те же проверки на entity-объекте |

**Паттерн «CHandle в глобале»:**

```lua
local saved_handle = nil
events.create_move:add(function(cmd)
    local me = game.local_player()
    if me.valid then saved_handle = me.handle end
    if saved_handle then
        local fresh = game.entity_by_handle(saved_handle)
        if fresh then print(fresh.health) end
    end
end)
```

Все `game.get_*` и `p.*`-чтения идут через `safe_read` — даже если адрес
уже протух, вернётся `nil`/`0`, а не краш.

**Strict-подсказка (warn-once):** обращение к полю, которого нет — `p.m_iHealt`
(опечатка), неизвестное свойство или `m_*`, не нашедшееся в схеме после патча —
возвращает `nil` как раньше, НО один раз на ключ за сессию пишет в консоль
чита: `entity.'m_iHealt' = nil — поле не найдено (опечатка? поле пропало после
патча?)`. Молчаливый `nil` в hot-path остаётся без спама — сообщение одно.

### Поля entity-объекта (полный список)

| Поле/метод | Тип / возврат |
|---|---|
| `p.valid`, `p.alive`, `p.is_local`, `p.dormant`, `p.is_enemy` | bool |
| `p.health`, `p.team`, `p.flags`, `p.index` | integer |
| `p.ptr` / `p.address`, `p.controller` | integer (указатель числом) |
| `p.origin`, `p.velocity` | Vector3 |
| `p:network_origin()` | Vector3 — сырая сетевая позиция (m_vecOrigin), не интерполированная — для backtrack/desync |
| `p:hitbox_set()` | `{{id, bone, mins, maxs, radius}, ...}` — полный хитбокс-сет модели |
| `p.name`, `p.weapon` | string |
| `p:bone(id)` | Vector3 (7 = голова) |
| `p:eye()`, `p:center()`, `p:hitbox(id)`, `p:bbox()` | Vector3 / таблица |
| `p:pose_params()` | {24 числа} или nil (m_flPoseParameter). Эмпирическая карта индексов ниже — «pose_params: карта» |
| `p:distance_to(other)`, `p:is_visible()`, `p:weapon_entity()`, `p:ammo()`, `p:reloading()`, `p:weapon_id()` | смешанные |
| `p:netvar(name)` / `netvar_int/_float/_bool/_vec3` | по схеме Source 2 |
| `p.m_iHealth` и любые `m_*` | динамический `__index/__newindex` через схему |
| `p:set_glow/set_chams/clear_visuals`, `p:set_paint_kit/set_stattrak/set_wear/set_name_tag/clear_skin` | скины/визуалы |
| `p:animstate()` | таблица анимационного состояния (см. ниже) или nil |
| `p:anim_layers()` | массив слоёв анимации или nil (см. ниже) |

### animstate / anim_layers

```lua
local st = p:animstate()       -- nil, если pawn кривой или схемы нет
-- st: eye_angles {pitch,yaw,roll}, duck_amount (0..1), velocity vec3
local layers = p:anim_layers() -- { {sequence, cycle, playback_rate, loop_mode, finished}, ... }
```

- `p:animstate()` → `{eye_angles, duck_amount, flags, velocity, anim_time}` — nil без схемы
- `p:anim_layers()` → **смена семантики (важно!)**: в старых итерациях
  `anim_layers` возвращал массив 15 слоёв `{cycle, weight, order}` из
  CAnimationLayer — в AnimGraph2 CS2 этого массива больше **нет**. Сейчас
  возвращается состояние активной последовательности **главного анимграфа**:
  `{sequence, cycle, playback_rate, loop_mode, finished}` (массив из одного
  слоя, nil без схемы). Смещение цепочки контроллера резолвится **по схеме**
  (m_pMainGraphController — наследование C_CSPlayerPawn → CBaseAnimGraph),
  хардкода нет — живёт при обновлениях игры.

  Миграция старых скриптов: если вы использовали `layers[i].weight/order` для
  desync/fakelag-индикатора, теперь читайте `layers[1].sequence /
  layers[1].playback_rate` (активная последовательность и её скорость) —
  старые поля не существуют в современном движке и восстановлению не
  подлежат.
- для backtrack-ESP кости уже есть: `lagcomp.records(pawn)` (27 bones), pose_params не нужен

### pose_params: карта (эмпирически)

Индексы `p:pose_params()` зависят от модели, но для стандартных CT/T-моделей CS2
карта стабильна (может сдвинуться после патча — проверяйте):

| Индекс | Что | Диапазон |
|---|---|---|
| `[0]`  | body yaw (разворот таза) | 0..1 (0.5 = прямо) |
| `[1]`  | body pitch (наклон туловища) | 0..1 |
| `[2]`  | lean yaw (наклон вбок) | 0..1 |
| `[3-8]`| торс/руки (промежуточные позы) | 0..1 |
| `[9]`  | aim pitch (камера вниз/вверх) | 0..1 (0.5 = горизонт) |
| `[10]` | head yaw | 0..1 |
| `[11]` | head pitch | 0..1 |
| `[12-23]` | оружие/аксессуары (нож/гранаты) | 0..1 |

Проверка после патча: сравните на живом игроке с m_angEyeAngles —
`[9] ≈ pitch / -89 + 0.5`, `[10] ≈ yaw / 360 + 0.5`. Если не сходится —
используйте `p:animstate()` + `p:anim_layers()` вместо pose_params.

## entities

Fatality-стиль. **Не храните энтити в глобалах** — храните указатель (CHandle)
и берите заново.

| Функция/поле | Описание |
|---|---|
| `entities.get_local_pawn()` | локальный pawn (camelCase-версии НЕТ — только snake_case) |
| `entities.get_local_controller()` | локальный контроллер |
| `entities.players` / `items` / `projectiles` | списки: `:for_each(fn)`, `:for_each_z(fn)`; `entities.controllers` — алиас `players` (список павнов), `entities.dropped_items` — алиас `items` |
| `list:for_each(fn)` | fn({entity=объект, handle=CHandle}) — только живые игроки (hp>0) |
| `list:for_each_z(fn)` | то же, но без фильтра «только живые» — все записи кэша |

## view / input.set_blocked

Свободная камера: скрипт отрывает камеру и глушит ввод.

```lua
events.override_view:add(function()
    view.set_origin({x, y, z})     -- позиция камеры
    view.set_angles({p, y, r})     -- углы
    view.set_fov(90)               -- fov/aspect/z_near/z_far тоже есть
    input.set_blocked(true)        -- глушить ввод персонажа, пока фрекам
end)
```

Геттеры `view.get_*` читают текущий CViewSetup только внутри события.
`input.is_blocked()` — текущее состояние. `view.ptr()` → integer (адрес CViewSetup
для ffi/mem-копания; свежий только внутри события).

---

## events

Подписка: `events.<имя>:add(fn[, priority])` → cookie (число). Снятие:
`events.<имя>:remove(cookie)` — по числу из `add`; `remove(fn)` — первая
подписка с этой функцией (identity-сравнение); `remove()` без аргументов —
ВСЕ подписки события. Алиасы `event:Add()/Remove()` тоже работают.

**Приоритеты** (необязательный второй аргумент): больший `priority` — колбэк
вызывается раньше; равные — в порядке добавления. По умолчанию 0:

```lua
events.createmove:add(function() ... end, 10 )  -- раньше всех
events.createmove:add(function() ... end, -5 )  -- после дефолтных
```
**Любое** имя авто-создаётся (mods.events-стиль) — можно подписаться на любое
игровое событие движка даже без явного списка.

Aliased-имена: `events.createmove` (= create_move), `events.present` / `presentQueue`
(= present_queue), `events.fsn` (= frame_stage_notify), `events.paint` (= draw),
`events.handle_input` (= input), `events.game_event` (= event).

**`events.list()`** → массив имён всех известных событий (встроенные + созданные
скриптом). Опечатка находит себя так:

```lua
local names = events.list( )
local found = false
for _, name in ipairs( names ) do
    if name == 'player_hurt' then found = true end
end
if not found then
    print( 'player_hurt не в списке — проверьте, подписываетесь ли на правильное имя' )
end
-- или просто валидация ДО подписки:
if not events.has( 'player_hurt' ) then print( 'event missing' ) end
```

Если вы подписываетесь на имя с цифрой или короче 3 символов — в консоль
пишется warning «найденная ручная подписка на неизвестное событие» —
первый сигнал ТОЛЬКО явного мусора (`a1`, `xyz`). Похожие имена (`player_hurtt`
против `player_hurt`) warning не ловят — проверяйте через `events.has()`
и `events.list()`.

| Событие | Аргумент | Когда |
|---|---|---|
| `present_queue` / `presentQueue` | — | каждый кадр рендера (основное место для 2D-рисования; полный список каналов, разрешающих draw.*, см. Render contract ниже) |
| `draw` | — | каждый кадр (алиас on_paint) |
| `create_move` / `createMove` | cmd | каждый тик, до фич — можно менять cmd |
| `createmove_post` | event | после фич |
| `frame_stage_notify` / `frameStageNotify` | stage (число) | смена стадии кадра; константы `events.frame_stage`: `FRAME_START=0`, `FRAME_NET_UPDATE_START=1`, `FRAME_NET_UPDATE_POSTDATAINPUT_START=2`, `FRAME_NET_UPDATE_POSTDATAINPUT_END=3`, `FRAME_NET_UPDATE_END=4`, `FRAME_RENDER_START=12`, `FRAME_RENDER_END=13`, `FRAME_COUNT=14` |
| `render_start_pre` / `render_start_post` | — / setup | сцена рендера |
| `setup_view_pre` / `setup_view_post` | — | установка вью |
| `override_view` / `overrideView` | setup | переопределение камеры (см. view.*) |
| `input` | msg, w, l | ввод GUI (Win32) |
| `event` / `game_event` | e `{get_int, get_string, get_float, get_name}` | любое игровое событие |
| `<имя события движка>` | e | player_hurt, player_death, weapon_fire, bullet_impact, round_start/end, bomb_*, item_purchase, player_spawn, chat_message, vote_*, ... |
| `shot` / `hit` / `miss` | t `{tick, pawn, damage, health, hitgroup, hitchance, inaccuracy, backtrack_ticks, extrapolated, lethal, forced, hit}` | выстрел/попадание/промах |
| `round_start` | — | уровень загружен (post) |
| `pre_round_start` | — | смена карты началась (до инициализации уровня) |
| `menu_toggled` / `menuToggled` | — (видимость через gui.IsVisible()) | открытие/закрытие меню |
| `player_info_pre` / `playerInfoPre` | p `{pawn, name, health, armor, enemy, visible, dormant, x, y, w, h}` | ДО отрисовки ESP игрока |
| `player_info_post` / `playerInfoPost` | p (тот же payload) | ПОСЛЕ ESP — рисуйте поверх |
| `generate_primitives` | ctx `{entity}` + методы `ctx:override_material(mat)`, `ctx:set_color(r,g,b,a)`, `ctx:suppress()` | Custom Chams-пайплайн |
| `sound` | s `{name, pos (vec3), pawn, ptr}` | позиционные звуковые события: `player_footstep`, `weapon_sound`, `item_pickup` — живой стрим для sound ESP; `s.pawn` может быть 0 у item_pickup |
| `entity_created` / `entity_removed` | t `{entity, handle, class}` | цепочка энтити-кэша: сущность добавлена/удалена (единственное окно, где pawn уже в кэше и жив / ещё читаем) |
| `unload` | — | выгрузка скрипта (ошибки НЕ вызывают) |
| `error` | err `{...}` → вызывается как `fn(error_text, event_name)` | lua-ошибка в колбэке/`on_paint` — вызывается в СВОЁМ стейте ДО остановки скрипта (при ошибке `unload`/`__shutdown` не срабатывают — это единственный шанс залогировать сбой). На SEH-падение не вызывается |
| `post_network_data_received` | — | пакет пришёл с сервера, снапшот сущностей обновлён — самые свежие данные до renderа |

```lua
events.player_hurt:add(function(e)
    -- userid события ≠ entity-объект: сопоставляем через GetController (см. §38)
    local attacker = e:GetController('attacker')
    if attacker and attacker.is_local then
        print('hit!', e:get_int('damage'))
    end
end)

-- events.error: самодиагностика падения скрипта
events.error:add(function(err, where)
    print('script error in ' .. where .. ': ' .. err)
end)
```

## draw

2D-рисование (только в present_queue/draw/esp-событиях). Цвет:
`draw.rgb(r, g, b, a)`, `draw.argb(ARGB)`, строка `"#rrggbb[aa]"`, число,
или таблица `{r,g,b,a}`.

**Единица цвета — 0..255 ВЕЗДЕ.** Таблица `{r,g,b,a}` клампится в 0-255:
`{r = 0.9}` — это почти чёрный (0), а не «90% яркости». Канонический
конструктор — `draw.rgb(255, 128, 0, 200)`.

| Функция | Описание |
|---|---|
| `draw.line(x1, y1, x2, y2, col, [th])` | линия |
| `draw.polyline(pts, col, [th])` | ломаная |
| `draw.chart(x, y, w, h, {values}, col, [th])` | график массива значений в прямоугольнике (нормализация min/max) — velocity/fps/hitchance без велосипеда |
| `draw.rect_filled / rect_gradient(x, y, w, h, col)` | напрямую рисуемые прямоугольники (w/h-семантика) |
| `draw.rect(x, y, w, h, col, [th], [rounding])` | примитив: рисует прямоугольник (x, y, w, h, цвет, толщина, скругление). Для Fatality-rect-объекта используйте `draw.rect_obj(x0, y0, x1, y1)` |
| `draw.rect_gradient(x, y, w, h, c1, c2)` | градиент |
| `draw.circle / circle_filled(x, y, r, col)` | круги |
| `draw.triangle(x1, y1, x2, y2, x3, y3, col)` | треугольник |
| `draw.text(x, y, text, col, [align])` | текст; align: `"center"` / `"right"`. **5-й аргумент может быть opts-таблицей**: `{ align = "center", font = draw.fonts['ключ'] }` — кастомный шрифт прямо в draw.text |
| `draw.gradient4(x, y, w, h, c1, c2, c3, c4 [, rounding])` | 4-цветный градиент по углам (tl, tr, bl, br), опционально скруглённый |
| `draw.text_outlined(...)` | текст с обводкой |
| `draw.measure(text)` | ширина, высота текста |
| `draw.world_text(pos, text, col)` / `world_line(a, b, col)` | 3D→экран |
| `draw.arc(x, y, r, a0, a1, col)` | дуга |
| `draw.bezier(p0, p1, p2, p3, col)` | кривая |
| `draw.push_clip(x, y, w, h)` / `pop_clip()` | клип-область |
| `draw.with_clip(x, y, w, h, fn)` | RAII-клип: push → fn() → гарантированный pop (см. Render contract) |
| `draw.begin_batch() / end_batch()` | батчинг примитивов |
| `draw.pending()` | число непроигранных примитивов (диагностика батча) |
| `draw.screen_size()` | `{w, h}` |
| `draw.GetTime()` / `get_time` / `draw.time` | время рендера (сек; поле — свежее при каждом чтении) |
| `draw.GetFrameTime()` / `draw.frame_time` | дельта кадра |
| `draw.GetScale()` / `draw.scale` | DPI-множитель |
| `draw.GetDisplay()` / `draw.display` | `{x, y}` размер экрана |
| `draw.device_ptr()` / `draw.GetDevice()` | lightuserdata ID3D11Device* текущего рендера (для ffi.cast) |
| `draw.rtv_ptr()` / `draw.GetBackBuffer()` | lightuserdata ID3D11RenderTargetView* — тот RTV, куда пишет наш пайплайн (не хранить между кадрами) |
| `draw.rgb(r,g,b,a)` | цвет числом; `draw.color.white/black/gray/interpolate` |
| `draw.vec2(x,y)` | вызываемая таблица Fatality (vec2-конструктор). Для прямого примитива отрисовки используйте `draw.rect(x, y, w, h, col, [th], [rounding])`. Для Fatality-rect-объекта используйте `draw.rect_obj(x0, y0, x1, y1)` |
| `draw.rounding`, `draw.outline_mode`, `draw.text_alignment`, `draw.shadow_dir` | енумы: `rounding` — битовая маска: tl=1, tr=2, br=4, bl=8, t=3, l=9, r=6, b=12, all=15; `outline_mode`: inset=0, outset=1, center=2; `text_alignment`: left/top=0, center=1, right/bottom=2; `shadow_dir`: up=0, down=1, left=2, right=3 |
| `draw.shader(hlsl_src)` | компиляция HLSL через D3DCompile (nil при ошибке); подробности и пример ниже — «### draw.shader» |

### color-объект (методы Fatality-стиля)

```lua
local c = draw.rgb(255, 128, 0, 200)   -- число... для объектных методов есть draw.color-объекты
```

Color-объекты (из `gui.GetThemeColors()` и `draw.color.*`-таблиц) несут методы:
`c:rgba()` / `c:argb()` → число; `c:red()/green()/blue()/alpha()` → компоненты;
`c:set_r/g/b/a(v)`, `c:set_rgba(r,g,b,a)`; `c:mod_a(f)` → число (множитель альфы);
`c:darken(k)` → число.

### Ассеты

```lua
local tex = draw.texture('имя')               -- из draw.textures-аксессора
local tex = draw.texture_bytes(png_bytes)      -- WIC: png/jpg
local tex = draw.texture_base64('iVBOR...')
local tex = draw.texture_raw(w, h, rgba8bytes)
local tex = draw.svg_texture('<svg .../>', scale)  -- = CreatePanoramaSvgTexture
local font = draw.font('имя', size)            -- из draw.fonts-аксессора
local atlas = draw.atlas('img.png', 4, 4)
local uv = atlas:get(3, 2)                     -- {u0, v0, u1, v1}
draw.image_uv(atlas, x, y, 32, 32, uv.u0, uv.v0, uv.u1, uv.v1, col)
draw.builtin_icon('icon_rage', x, y, size)     -- встроенные иконки GUI
```

**Картинки: `draw.image` как функции НЕ существует** (частая ловушка). Три
рабочих пути: `draw.image_uv(atlas, ...)` (выше), `draw.surface:add_image(...)`
(следующая секция), и `draw.texture_bytes/texture_base64` для загрузки PNG из
памяти — дальше используйте результат как `atlas` в `image_uv`.

### draw.shader — полная спецификация

`draw.shader(hlsl_src)` компилирует **пиксельный** шейдер (профиль `ps_4_0`,
точка входа `main` — фиксированы). Ошибка компиляции → `nil` + лог
`draw.shader compile error: ...` (текст D3DCompile с номером строки) в консоль
чита. **Вершинные** шейдеры из Lua недоступны — пайплайн рисует геометрию своим
фиксированным VS (passed-through position/uv, профиль `vs_4_0`).

Что видит шейдер на входе:
- `float4 pos : SV_POSITION` — экранные координаты квада (px)
- `float2 uv  : TEXCOORD0` — 0..1 по прямоугольнику (0,0 — верх-лево)

Выход — `float4 : SV_TARGET`. Uniform-константы из Lua передать нельзя; зашитый
cbuffer несёт `float t` (время в секундах от старта чита) и `float w, h`
(размер вьюпорта в px): время/экран доступны шейдеру, остальное — байт в текст
шейдера прямо в исходник.

**ВАЖНО: компилятор подставляет префикс ЗА вас.** Перед вашим исходником
D3DCompile автоматически получает:

```hlsl
cbuffer Params : register(b0) { float t; float w; float h; };
```

Поэтому `t`/`w`/`h` доступны в вашем коде без объявления. Не объявляйте
cbuffer b0 сами — получите ошибку переопределения. Полный компилируемый пример:

```lua
-- функция обязана называться main, точка входа фиксирована
-- t/w/h приходят из зашитого cbuffer (префикс выше — подставляется сам)
local sh = draw.shader([[ float4 main(float4 pos: SV_POSITION, float2 uv: TEXCOORD0): SV_TARGET
    { return float4(uv.x, uv.y, 0.5 + 0.5*sin(t), 1); } ]])
if not sh then print('shader compile failed — см. лог ошибки') end
```

Применение — поверх surface-квада (порядок аргументов surface-методов: x0,y0,x1,y1, tex, uv):
```lua
local img = draw.texture('my_pic')
events.present_queue:add(function()
    if sh then
        draw.surface.g:set_shader(sh)
        draw.surface:add_image_uv(10, 10, 138, 138, img, 0, 0, 1, 1)  -- квад с шейдером
        draw.surface.g:reset()
    end
end)
-- surface + set_shader рисует квад с шейдером в конце кадра, состояние
-- пайплайна чита восстанавливается автоматически (пост-обработка не нужна)
```

## draw.surface — Layer-API Fatality

`draw.surface:Метод(...)` — координаты **два угла** (x0,y0,x1,y1) у rect:

`AddRect, AddRectFilled, AddRectFilledMultiColor, AddRectFilledRounded,
AddRectRounded, AddCircle, AddCircleFilled, AddTriangle, AddTriangleFilled,
AddTriangleFilledMultiColor, AddQuad, AddQuadFilled, AddQuadFilledMultiColor,
AddLine, AddLineMultiColor, AddText, AddImage, AddGlow` + snake_case алиасы
(`add_rect_filled_rounded` и т.д.), `add_image_uv`, `add_shadow_rect/add_shadow_line`,
`override_clip_rect / reset_clip_rect`.

`draw.surface.g` — состояние: `g:set_texture(tex)`, `g:set_shader(shader)`, `g:reset()`.

```lua
events.present_queue:add(function()
    local c = gui.GetThemeColors()
    draw.surface:AddRect(10, 10, 210, 30, c.accent)
    draw.surface:AddRectFilled(10, 44, 210, 8, c.bgBlock)
end)
```

## draw.textures / draw.fonts — Accessor-стиль

`draw.textures['ключ']` и `draw.fonts['ключ']` тянут ресурс через
`draw.texture/draw.font` и кэшируют. Push своих — просто присвойте,
получив handle от `texture_bytes/svg_texture/font`.

Встроенные ключи Fatality работают: `gui_main`, `gui_bold`, `gui_debug`, ... (fonts).

## scene

3D-примитивы в мире (рисуются в present-фазе, честная проекция + клип по
near-plane):

| Функция | Описание |
|---|---|
| `draw.scene.line(a, b, col, [th])` | линия в мире |
| `draw.scene.polyline({p1, p2, ...}, col, [th])` | ломаная в мире |
| `draw.scene.text(pos, text, [col], [align])` | текст в мире (align: `"center"/"right"`) |
| `draw.scene.circle(center, radius, normal, col)` | круг по нормали |
| `draw.scene.cylinder(a, b, radius, col)` | цилиндр |
| `draw.scene.sphere(center, radius, col)` | сфера |
| `draw.scene.box(mins, maxs, col)` | бокс по углам |
| `draw.scene.hitbox(pawn, hitbox_id, col)` | хитбокс в мире |

---

## gui

Контролы-объекты с методами и доступ к настройкам чита.

| Функция | Описание |
|---|---|
| `gui.ctx:Find('rage>aimbot>...')` | нечёткий поиск настройки чита по пути → контрол (или `gui.ctx:find`) |
| `gui.control_id(id)` | id-объект |
| `gui.checkbox(id, def)` / `slider(id, lo, hi, def)` / `label(id, text)` / `selectable` / `button` / `color_picker` / `text_input` / `combo_box` / `image` / `spacer` | контролы скрипта (появляются в меню → Scripts) |
| `gui.MakeControl(text, ctrl)` / `make_control` | строка «label + контрол» |
| `gui.MakeControlEasy(id, label, type, ...)` | `'checkbox'/'slider'/'color_picker'/'text_input'/'combo_box'` → `(element, layout)` |
| `gui.MakeStackedGroups(id, size, {grp, ...})` | вертикальный стак групп |
| `gui.IsVisible()` | открыто ли меню |
| `gui.GetThemeColors()` | 23 цвета темы: `accent, text, textMid, bgBlock, danger, warning, success, ...` — каждое `{r,g,b,a}` |
| `gui.GetMainWindow()` | `{ get_pos_abs() -> x, y; get_size() -> w, h }` |
| `gui.GetHotkeyList()` | кейбинды скрипта (контролы kind=keybind) |
| `gui.GetActiveOverridePath()` | `'rage>weapons><оружие>'` активного оружия |
| `gui.notification(header, text)` / `gui.notify:add(fn)` | уведомления |
| `gui.input.*` | `cursor/cursor_prev`, `is_key_down/up/clicked/released`, `is_mouse_*`, `wheel_delta` |

Методы контрола: `get_value()/set_value(v)`, `get_raw()/set_raw`, `set_visible(b)`,
`is_visible()`, `set_text(s)`, `add_callback(fn)`, `add(ctrl)`, `reset()`,
`get_type()`, `get_parent()`, `value:get()/set/get_raw/set_raw` (у value-объектов).

```lua
local cb, row = gui.MakeControlEasy('my_cb', 'Enable glow', 'checkbox', true)
cb:add_callback(function() print('toggled', cb:get_value()) end)
```

## ui

Процедурный стиль (id-строки):
`ui.checkbox(id, label, def)`, `ui.slider_int/slider_float(id, label, def, min, max)`,
`ui.combo(id, label, items, def)`, `ui.keybind(id, label, def)`, `ui.color(id, label, def)`,
`ui.text(id, label)`, `ui.get(id)`, `ui.set(id, v)`, `ui.list()`,
`ui.set_visible/get_visible(id)`, `ui.set_label/get_label(id)`, `ui.new_tab(name)`.

Gamesense-стиль (поверх): `ui.new_checkbox/new_slider_int/new_slider_float/new_combo/
new_combobox/new_color/new_color_picker/new_keybind/new_hotkey/new_button/new_label/
new_textbox(group, name, ...)` → возвращают id-строку; `ui.reference(group, tab, name)`
→ `{get, set}` если контрол уже объявлен.

## cheat

| Функция | Описание |
|---|---|
| `cheat.key_pressed(vk)` / `cheat.key_released(vk)` | одноразовые события клавиш (edge detection) — зовите из `events.draw`; `pressed` срабатывает ровно раз на нажатие |
| `cheat.hook(name, fn)` | **хук пайплайна**: `fn(next, ...)` → число (переопределить), `false` (запретить), nil (дефолт). Точки — см. ниже |
| `cheat.unhook(name[, idx])` | снять хуки |

**Все точки `cheat.hook`:**

| Точка | Аргументы | Возврат (что переопределяет) |
|---|---|---|
| `rage.can_shoot` | — | число ≥1 — разрешить выстрел, `false`/0 — запретить |
| `rage.hitchance` | value (текущий %) | новый % (0-100) или nil |
| `rage.find_target` | массив кандидатов (указатели пешников) | указатель выбранного пешника или nil |
| `rage.aim_point` | vec3 цель | новый vec3 (сдвинуть точку) или nil |
| `rage.damage` | damage (текущий расчётный урон) | новый урон или nil |
| `rage.penetration` | (penetrated, damage) per-sample | вклад сэмпла в hitchance (0..1) или nil |
| `rage.pre_fire` | таблица `{hitchance, min_damage, ...}` | новая таблица с теми же полями или nil |
| `antiaim.pitch` | pitch | новый pitch или nil |
| `antiaim.yaw` | yaw | новый yaw или nil |
| `antiaim.desync` | desync | новый desync или nil |
| `antiaim.override` | — | итоговый yaw (или nil = дефолтный AA) |
| `createmove.pre` | — (game.cmd_SET_* работают) | `false` — скрипт забирает команду: все встроенные фичи пропущены |
| `createmove.post` | — (финальная команда готова) | `false` — финальный субтик/autostop-finalize/AA-finalize/anti-AFK пропущены, команда уходит как написал скрипт |
| **`misc.bhop`** | — | `false` — запретить прыжок bhop на этом тике (переопределение встроенного bhop) |
| **`misc.autostop`** | — | `false` — не стопить движение на этом тике (переопределение autostop) |
| **`misc.autoscope`** | — | `false` — отменить пресс зума на этом тике (переопределение autoscope) |
| **`entity.pre_create`** | entity, handle | `false` — ОТМЕНИТЬ создание энтити: не попадёт ни в кэш, ни в движок (вето на спавн пропа/гранаты). **ОПАСНО**: движок может рассчитывать на успешную регистрацию |
| **`entity.pre_remove`** | entity, handle | `false` — отменить удаление энтити. **ОПАСНО вдвойне**: рассинхрон lifecycle сервер/клиент — только эксперименты |
| `render.player` | таблица `{pawn, name, health, armor, enemy, visible, dormant, x, y, w, h}` | `false` — подавить встроенный ESP pawn'а (алиас esp.player) |
| `render.world` | — | только доп-слой: рисуй draw.*/scene.* поверх; возврат не гейтит |
| `esp.player` | таблица `{entity, x, y, w, h, ...}` | таблица-ответ с полями или nil |
| `esp.players` | batch — массив строк по всем игрокам | как выше, но один вызов на кадр |
| `esp.fov` | value | новый FOV или nil |

Доступ к ЛЮБОЙ настройке меню по имени (поиск имён — `cheat.vars`):

| Функция | Возврат |
|---|---|
| `cheat.vars([filter])` | `{имена}` настроек чита (фильтр — подстрока; скриптовые ui-контролы в реестре НЕ числятся — только настройки самого чита) |
| `cheat.get_var(name)` | value, type — двумя возвратами: `(значение, "тип")`; type — строка `"bool" / "int" / "float" / "color" / "vec3" / "bool_array" / "string" / "setting"` |
| `cheat.set_var(name, value)` | bool. Тип не совпал (например, число в bool) — контрол приводится тихо: число ≠ 0 → true; строка в число — не приводится, `false` |
| `cheat.var_type(name)` | string (тот же формат, что второй возврат get_var) |
| `cheat.GetCheatVar` / `SetCheatVar` / `RegisterCallback` / `GetCheatVars` | алиасы тех же вызовов |

Чтение/запись по типам:
- **bool / int / float / string** — числа и строки как есть;
- **color** — читается всегда таблицей `{r, g, b, a}` (0-255, каждый компонент!); пишется таблицей `{r,g,b,a}` ИЛИ числом;
- **vec3** — таблицей `{x, y, z}` в обе стороны;
- **bool_array** (например, hitboxes) — читается `{bool,...}` по группам; пишется массивом ИЛИ одним bool на все элементы;
- **setting** — булевый контрол меню, работает как bool;
- неизвестное имя — `get_var` → `nil, "unknown variable name"`, `set_var` → `false`.

```lua
-- минимальный контракт за 5 строк:
local val, ty = cheat.get_var("ragebot.hit chance")  -- val = 80 (int) по умолчанию
type( val )                                          -- "number"
type( ty )                                           -- "string": "int"
cheat.set_var("ragebot.hit chance", 90)              -- true
local col = cheat.get_var("weather.fog color")        -- color вернётся ОДНОЙ таблицей {r,g,b,a}
local b, g, r, a = col.b, col.g, col.r, col.a         -- разбираем полями, НЕ множественным возвратом
```

Прочее: `cheat.log(...)` — вывод в консоль (как print), `cheat.set_safe_mode(bool)`,
`cheat.safe_mode()` — состояние, `cheat.profile(tag, fn)` — профилировщик блоков,
`cheat.script_name()`, `cheat.key_down(vk)`, `cheat.set_script_budget(ms)` /
`set_memory_limit_mb(mb)`, `cheat.reload_script(name)`.

| `cheat.register_callback(name, fn)` | алиас events[name]:add |
| `cheat.kv_get/kv_set/kv_save` | общее KV-хранилище (persists в kv.json) |

### debug.stack (отладка)

`debug.stack([limit])` → `{фреймы}` (1–30): строки вида `"scripts/x.lua:42: in function ..."`.
`debug.traceback()` остаётся в песочнице (используется как message-handler ошибок).

```lua
local frames = debug.stack(5)
for i, f in ipairs(frames) do print(i, f) end
```

---

## rage / antiaim {#rage-antiaim}

Полный гет/сет поверх конфига: `get/set_enabled, hitchance, min_damage, max_fov,
priority_head, lethal, backtrack, extrapolation`, `get_target()`,
`get_group() -> 'pistol'/'smg'/'rifle'/...`,
`get/set_silent, flick, no_spread, doubletap, body_aim, force_shot_air, force_shot`,
`get/set_point_scale (1-100), min_damage_override(+_value 1-130), hitchance_override(+_value 1-100)`,
`get/set_hitboxes / {head, chest, stomach, arms, legs, feet}`, `get/set_humanize`,
`get/set_max_backtrack_ticks / max_extrapolate_ticks (1-64)`.

`rage.pre_fire(fn)` — колбэк перед выстрелом (динамический min damage и т.п.).

`rage.get_lagcomp()` — сводка отката без рекордов: `{backtrack_ticks,
extrapolated_targets, extrapolated_ticks, tracked_targets, valid_records,
available_ticks, extrap_reason}` (полные данные — `lagcomp.*`).

`rage.stats()` — живая статистика сессии по реальным событиям:

```lua
local s = rage.stats()
print(s.shots, s.hits, s.misses, s.accuracy)   -- accuracy = hits/shots
print(s.spread, s.lag_compensation, s.impact_mismatch, s.occlusion,
      s.extrapolation, s.other)                 -- миссы по причинам
```

`rage.stats_reset()` — обнулить сегмент вручную (авто-сброс на раунде/смене карты).

`rage.hold_reason()` → nil|`{reason, code, hits, age}` — WHY триггер держит выстрел.
Публикуется напрямую из `run_gun` (первый несработавший элемент гейта):

```lua
local h = rage.hold_reason()
if h then print(h.reason, h.code, h.hits, h.age) end
-- h.reason  — числового кода может не знать; человекочитаемая фраза
-- h.code    — стабильный номер: 0 none, 1 no_aim_point, 2 autoscope_wait,
--             3 fire_not_allowed, 4 shot_cooldown, 5 jumpscout_block, 6 duck_hold,
--             7 duck_peek_wait, 8 hitchance_below, 9 stop_not_confirmed,
--             10 fire_gate_closed, 11 fire_gun_declined, 12 world_gate_rejected,
--             13 silent_flick_windup, 14 weapon_cooldown, 15 other
-- h.hits    — сколько попаданий нашли сканом в момент холдa
-- h.age     — тиков с момента публикации (0 = только что)
```

После выстрела поле обнуляется (nil) — причина не «висит» поверх успеха.

`rage.dt_charge()` → `{ready(bool), charge(0..1), weapon_ready(bool)}` —
состояние заряда doubletap для HUD-индикаторов.

`rage.stats_extended()` → `{hold_code, miss_streak, hc_bonus, stop_held,
backtrack, stopping}` — живая механика меж выстрелами: адаптивный HC-порог,
стрик миссов, автостоп. Боевая диагностика в одном месте.

`rage.predict_origin(pawn, ticks)` → vec3|nil — линейное предсказание позиции
(origin + velocity * ticks * tick_interval). `rage.predict_hitbox(pawn, hitbox, ticks)`
→ vec3|nil — центр хитбокса из текущего скелета + сдвиг на предсказанный origin.
Не симуляция: просто экстраполяция текущей скорости; полноценное предсказание
чока — `lagcomp.records(pawn)` (extrapolated-рекорд с костями).

Поведение селектора (не API) — вынесено в отдельную секцию: [35. Rage: поведение селектора](#rage-поведение-селектора-не-api).

antiaim: `get/set_enabled, pitch, yaw, yaw_range, yaw_speed, max_yaw_delta (0-180),
subtick_desync, noise, noise_amount (0-10), at_target, freestanding,
freestanding_method (0-2), hide_shots, avoid_backstab, auto_yaw_adjust, manual_arrows`.

`antiaim.override(fn)` — вернуть итоговый yaw (или nil) поверх встроенного AA.

`antiaim.set_manual_side("left"|"right"|"forward"|"back")` / `get_manual_side()` —
manual-страницы через настройки manual-стрелок ("auto" — когда ни одна не активна).

Desync Indicator (данные прошлого тика, состояние AA, не конфиг):

- `antiaim.get_real_yaw()` → yaw реального взгляда (view);
- `antiaim.get_fake_yaw()` → поза, как AA её собирается отправить (m_pose_yaw); вне AA — реальный yaw;
- `antiaim.get_desync_factor()` → -1..1 (оффсет / 58-градусный максимум CS2) для полоски;
- `antiaim.get_desync_offset()` → оффсет относительно взгляда в градусах (со знаком);
- `antiaim.is_active()` → bool — AA отработал на последнем тике.

```lua
events.draw:add(function()
    if not antiaim.is_active() then return end
    local factor = antiaim.get_desync_factor()
    local fake, real = antiaim.get_fake_yaw(), antiaim.get_real_yaw()
    -- таблица цвета ВСЕГДА 0-255 (draw.clamp до 255): {r=0.9} это почти чёрный, не 90%
    draw.rect_filled(10, 10, 10 + 60 * math.abs(factor), 4, { r = 50, g = 230, b = 100, a = 255 })
end, 0)
```

## Rage: поведение селектора (не API)

Что бот делает сам (не настройки API, а поведение):

- **присед цели = duck hold С ТАЙМАУТОМ**: голова держит селектор до 48 тиков
  (~0.75 с) с момента захвата; проходящая чистая голова продлевает окно
  непрерывно; после таймаута тело играет снова (бесконечное «стопит и не
  стреляет пока нету головы» убрано — окно ограничено);
- **летальное тело пробивает любые ожидания**: если у присевшей/прикрытой
  цели есть убивающий body-поинт, а голова не летальна — выстрел в тело
  происходит немедленно (kill > ожидание головы; chip-голова никогда не
  держит летальное тело);
- **таймаут ожидания головы**: priority head без летальной головы держит
  селектор 6 тиков (~94 мс), затем 12 тиков окно подавления (огонь не мигает
  «ждёт/стреляет») и тело играет по Best;
- **head center-only при приоритете головы**: краевые head-мультипоинты
  отключены — они лежат на границе head/neck хитбоксов, и сервер мог
  регистрировать их как шею/грудь (те самые «выбил 71-91 хп, цель
  выжила»). Центр головы = максимальный урон (falloff меряется до точки
  попадания на хитбоксе) и корректный hitgroup;
- **standstill HC-release**: игрок уже стоит, блокер — только хитшанс →
  контр-стрейф стойка отпускается немедленно (скорость ниже не упадёт,
  стоять на открытом бессмысленно — свободно двигайся/размеривайся);
- мисс с причиной `lag compensation` / `impact mismatch` / `extrapolation`
  временно усиливает давление свежести по этой цели И сужает список рекордов
  до свежайшего (+ экстраполяция) — старые позы перестают выбираться;
- **адаптивный хитшанс мягкий**: 4 не-форс мисса подряд поднимают требуемый
  HC на 3% за каждый следующий (кап +18%), попадание сбрасывает, time-decay
  1 с (64 тика) — после секунды без миссов порог возвращается к базе (больше
  не держит бота «вечно ждущим»);
- **lagcomp-штамповка**: seed-tick и wire всегда согласованы (включая
  prediction-shifted команды — системный спред-мисс под чоком устранён);
  выстрел со «старого глаза» штампует тик этого глаза (сервер отматывает
  origin стрелка к точке выстрела — класс «shoot position mismatch»);
  rewind-штамп = ровно отсканированная поза (tick + frac) в rage и legit;
- **chip-блок**: не-летальный выстрел по цели ≤100 HP держится до 8 тиков
  (~125 мс) в пользу летальной альтернативы — если летала нет, чип
  отстреливается (лучше 71-91, чем смерть стоя);
- **перепроверка хитшанса**: селектор ранжирует точки по 64 сэмплам, финальный
  гейт перепроверяет выбранную точку world-трейсами адаптивно: 128 (чистая
  линия с запасом >6%), 192 (у порога), 256 (пенетрация или порог ≤55%);
  селектор и гейт считают ОДИН порог;
- **FPS при прицеле**: скан при scoped-оружии режет backtrack до 4 рекордов,
  head-мультипоинты идут по бюджету 10 трейсов на кандидата (центры головы
  всегда), разбор хитбоксов модели кэшируется по model_handle;
- autoscope держит выстрел 2 команды после вставания зума (settling spreads);
- `antiaim.hide_shots` — реальная функция: на тике выстрела тик-вью несёт
  анти-аим позу вместо угла на цель (пуля летит по углу из shot-entry);
  работает в silent-режиме, отключается при no-spread;
- DT (doubletap) несовместим с no-spread и отключается вместе с ним;
- состояние стопа/счётчики полностью сбрасываются при смерти, round_start,
  смене карты.

API antiaim описан в секции 15 (`antiaim.*`: get/set_enabled, pitch, yaw,
yaw_range, ..., override(fn) — см. ToC).

---

## memory

`memory.get_module_base('client.dll')`, `memory.is_valid(addr)`,
`memory.pattern_scan(mod, pattern)`, `memory.get_vfunc(ptr, idx)`,
`memory.read_utlvector(addr, elem_size [, max])` — Source 2 CUtlVector/CNetworkUtlVectorBase
(elements +0x00, count +0x08) → `{ count = n, [i] = адрес_элемента_i }`; каждый элемент
читайте `memory.read_i32/u32/...` по адресу (CHandle=u32, указатель=u64) — m_hMyWeapons,
m_vecItems и прочие сетевые контейнеры без ручных оффсетов,
`memory.read_i8/u8/i16/u16/i32/u32/i64/u64/float/double/bool(addr)`,
`memory.write_*` (те же типы), `memory.read_string(addr, [max])`,
`memory.write_string(addr, s)`, `memory.read_vec3/write_vec3(addr, {x,y,z})`.

## schema / interfaces / materials / cvar

| Таблица | Описание |
|---|---|
| `schema.offset(class, field)` | смещение нетвара/схемы |
| `schema.get(ptr, class, field)` | значение по схеме |
| `schema.list(class)` | список полей класса |
| `schema.dump_entity(ptr [, max])` | дамп сущности: `{{name, offset, value}, ...}` — все поля живой сущности (реверс-инструмент) |
| `interfaces.get('mod:Name')` / `get_vfunc(ptr, idx)` | интерфейсы движка |
| `materials.create(name, kv3)` | свой материал (Custom Chams; применить: `ent:set_chams('имя')`) |
| `materials.ptr(name)` | сырой указатель |
| `cvar.get/get_int/get_string(name)`, `cvar.set(name, v)` | convars |

Схема-рефлексия на entity-объектах: динамический `__index/__newindex` —
`p.m_iHealth` читается/пишется через схему автоматически; методы `netvar()`,
`get_netvar`, `set_glow/set_chams/clear_visuals`, скины `weapon_entity`,
`set_paint_kit/set_stattrak/set_wear/set_name_tag/clear_skin`.

## http

`http.Get/Post/Delete/Download` — **синхронные** (блокируют кадр до ответа: из
колбэков и on_paint не вызывать). Асинхронные версии — `GetAsync/PostAsync/
DeleteAsync/DownloadAsync`: колбэк приезжает в главный поток (pump в tick).

Опции — таблица `{ headers = {имя=значение, ...} (≤16), content_type = '...' }`
(именно `content_type`; ключа `contentType`/`asTable` НЕ существует).
Заголовок `User-Agent` можно переопределить через `headers` — отдельного
заголовка с таким именем не будет.

```lua
-- Синхронно: body (строка) или nil + err вторым возвратом
local body, err = http.Get('https://example.com', { headers = { ['X-My'] = 'v' } })
local ok2, err2 = http.Post(url, data_str, { content_type = 'application/json' })
local ok3, err3 = http.Delete(url, data_str, { content_type = 'application/json' })

-- Асинхронно: колбэк (body, err); ≤8 параллельных запросов, тело ≤4 МБ
http.GetAsync(url, function(body, err) print(body) end)
http.PostAsync(url, utils.JsonEncode({...}), function(body, err) end)
http.DeleteAsync(url, utils.JsonEncode({...}), function(body, err) end)

-- Скачивание в файл: имя БЕЗ пути (санитайз [a-zA-Z0-9._-], кладётся в scripts/)
http.Download(url, 'file.bin')                                 -- синхронно → полный путь | nil+err
http.DownloadAsync(url, 'file.bin', function(path, err) end)   -- асинхронно
```

## utils

| Функция | Описание |
|---|---|
| `utils.log/print(...)` | вывод в консоль |
| `utils.Base64Encode/Decode(s)` | base64 |
| `utils.JsonEncode(tbl)` / `utils.JsonDecode(s)` | JSON (decode → nil если битое) |
| `utils.Murmur2(s)` / `utils.Fnv1a(s)` | хэши |
| `utils.FindExport(mod, exp)` / `utils.FindPattern(mod, pattern)` | адреса ('DE AD ? ? ? ? BE EF') |
| `utils.ClipboardGet()` / `utils.ClipboardSet(s)` | буфер обмена |
| `utils.ArrayToString(bytes)` / `utils.StringToArray(s)` | байты↔строка |
| `utils.GetUnixTime()` | unix-время |
| `utils.GetDate()` | `{year, month, day, hour, minute, second, daylightSavings}` |
| `utils.time_ms()` / `utils.time()` | часы: секунды/мс от старта чита |
| `utils.random_int(min, max)` / `utils.random_float(min, max)` | равномерное случайное число — джиттер/анимации визуалов |
| `utils.thread(fn)` / `utils.sleep(ms, fn)` | **одноразовый планировщик** (НЕ корутины): fn выполнится через задержку — тот же механизм, что `Delay`; для многотактных задач — `utils.spawn` |
| `utils.path_join/dirname/basename/extension/stem/normalize(a[, b])` | работа с путями: join — склеить, dirname — папка, basename — имя файла, extension — `.ext`, stem — имя без расширения, normalize — нормализовать |
| `utils.spawn(fn)` / `utils.CreateThread` | **корутина**: fn выполняется по кадрам; внутри — `utils.yield()/wait_frames(n)/wait_seconds(s)` |
| `utils.require(name)` / глобал `require(name)` | модуль из `scripts/`: `'lib.vector'` → `scripts/lib/vector.lua`, кэш, ≤1 MiB, только папка скриптов |
| `utils.FileWatch(paths, fn)` / `utils.FileWatchStop(id)` | следить за файлами: `paths` — строка или массив (до 8), fn(path) при изменении; относительные — от `scripts/`; опрос раз в 0.5с, снятие — `FileWatchStop(id)`; id от `FileWatch` |
| `utils.get_screen_size()` | `{w, h}` |
| `bench.Start(name)` / `bench.End(name)` / `bench.Report()` / `bench.Reset()` | профилировщик блоков (total/n/avg в консоль) |

### Корутины (utils.spawn)

```lua
utils.spawn(function()
    for i = 1, 10 do
        utils.wait_frames(1)      -- пауза на кадр
    end
    utils.wait_seconds(0.5)       -- пауза в секундах
    local res = { http.GetAsync('https://api.site/x', {}, function(s, d) res = { s, d } end) }
    while not res do utils.yield() end   -- async-паттерн
    print(res[1], res[2])
end)
```

yield/wait_* действуют только внутри utils.spawn (иначе — обычный coroutine.error).

### require — модули

`require('lib.vector')` грузит `scripts/lib/vector.lua` (кэш в `__spype_modules`,
второй require отдаёт из кэша). Модуль возвращает таблицу:

```lua
-- scripts/lib/vector.lua
local M = {}
function M.new(x, y, z) return { x = x, y = y, z = z } end
return M
```

Готовый пример — `bin/scripts/lib/vector.lua` (метатаблицы, dot/normalize/distance).

Файлы и локальная «БД» (всё в папке scripts):

| Функция | Описание |
|---|---|
| `utils.FileExists/FileRead/FileWrite(path[, bytes])` | файлы в папке scripts |
| `utils.FileRemove/FileRename(old, new)` | удаление/перенос |
| `utils.FileEnumerate(wildcard)` | `{имена}` по маске |
| `utils.FileCreateDirectories(path)` | папки |
| `utils.DbSave(tbl, path)` → bool / `utils.DbLoad(path, [default])` | локальная «БД» (JSON в kv) |

## Autocomplete: spype.d.lua (VSCode)

В корне репозитория лежит `spype.d.lua` — стабы для [Lua Language Server](https://github.com/LuaLS/vscode-lua):

```json
// .luarc.json в папке скриптов:
{ "workspace.library": [ "<путь к spype.d.lua>" ] }
```

Подключили → получаете автокомплит `game.local_player().:bone( )`, подсказки
типов Entity/Vector3/GameEvent/UserCmd прямо в VSCode. Обновляется вместе с API.

Хотите добавить свой тип — формат классов такой (10 строк):

```lua
---@class Entity
---@field valid boolean
---@field alive boolean
---@field health integer
---@field origin Vector3
---@field is_enemy boolean
---@field handle integer # полный 32-битный CHandle

---@return Entity|nil
function game.local_player() end
```

Пишите новый `---@class` + `---@field` на каждое поле, метод — `function
Класс:имя(арг) end` с `---@param`/`---@return`. Готовые живые образцы — сам
`spype.d.lua` (Entity/lagcomp/SoundEvent/ffi).

## ws

| Функция | Возвращает |
|---|---|
| `ws.GetItemID()` / `ws.GetBuildID()` | 0 (локальные скрипты) |
| `ws.GetTitle()` | имя скрипта |
| `ws.TestCapability('ffi'\|'clipboard'\|'fs'\|'http')` | все true в spype |
| `ws.GetResourceDir()` | папка scripts |

## sound

WinMM MCI, per-sound громкость, wav/mp3 из папки scripts:

```lua
local snd = sound.play('hit.wav', 0.5)  -- путь, volume 0..1 (или -1 = системная)
snd:set_volume(0.2) snd:pause() snd:stop()
print(snd:is_playing(), snd:get_position(), snd:get_duration())  -- статус MCI
sound.play_bytes(wav_data)              -- WAV из памяти
sound.play_at({x, y, z}, 'step.wav', 1.0)  -- громкость по дистанции
sound.stop_all()
```

Плейлисты/синхронизация с музыкой — через `:is_playing()`/`:get_position()`.

Звуковые события движка — два пути:
- именованный: `events.player_footstep:add(fn)` — обычный GameEvent
  (e:get_int("userid"), e:get_float("x")) — как раньше;
- сводный позиционный: `events.sound:add(function(s) ... end)` — `s = {name,
  pos (vec3), pawn, ptr}`; один хук на все `player_footstep` / `weapon_sound` /
  `item_pickup` сразу, готовая позиция для звукового ESP:

```lua
events.sound:add(function(s)
    -- s.pawn может быть 0 (item_pickup без владельца) — проверка обязательна
    if s.pawn ~= 0 and game.is_enemy(s.pawn) then
        draw.world_text(s.pos, '[' .. s.name .. ']', draw.rgb(255, 80, 80))
    end
end)
```

## chat / console

`chat.say(text)`, `chat.team_say(text)`, `chat.available()` — реальная отправка
(санитайз `\n ; "`). `console.exec(cmd)` — настоящий exec;
`console.get/get_int/get_string/set/exists(name)` — convars.

## binds / config / recorder

`binds.register(name, vk, 'hold'\|'toggle'\|'always', fn)` → cookie;
`binds.from_keybind(ui_id, mode, fn)`; `unregister/get/set/mode/is_active/list`.

`config.save/load/delete/list/rename(name)`, `config.export(name)` → JSON,
`config.import(name, json)`, `config.auto_save(bool)`, `config.get_current()` —
пресеты ui-переменных скрипта.

`recorder.*` — запись/replay движения: `start/stop/pause/resume/cancel/save/load/delete/list`,
`is_recording/is_playing/is_paused/get_length/get_position/get_angles/get_velocity`,
`mark/marks`, `play{speed, loop, on_finish}/stop_play`,
`draw_path/draw_ghost/draw_ticks`, `set_visualizer_offset(v)`, `compare(a,b)`,
`export(name, 'json'\|'csv')`.

## world / hud

`world.get/set_fog`, `get/set_skybox`, `get/set_wind`, `set_weather/get_weather`,
`set_lighting`, `set_world_color`, `set_bloom`, `set_gamma`, `add_light`.

`hud.get/set_crosshair(+_size/_color)`, `get/set_scope(+_length/_gap/_color)`,
`get/set_velocity(+_chart/_color)`, `get/set_slowdown(+_color)`, `get/set_hat(+_type)`.

## esp.* — управление ESP из скрипта

`esp.set_player_override(pawn, {disable=bool, r,g,b,a=bool+значения})` —
цвет/выключение встроенного ESP конкретного игрока;
`esp.disable_builtin()/enable_builtin()/clear_overrides()`.
Плюс хуки `esp.player` (per-player) и `esp.players` (batch) — полный кастомный ESP из Lua.

## Глобальные алиасы (верхний уровень)

Зарегистрированы глобально, без таблиц:

| Глобал | Эквивалент |
|---|---|
| `GetLocalPlayer()` | `game.local_player()` → entity-объект |
| `GetNetVar(ptr, field)` | `game.netvar` |
| `IsEnemy(ent)` / `Distance(a, b)` / `IsValidEntity(ptr)` / `GetEntities()` | `game.*` |
| `GetLocalInfo()` | `game.get_local()` (низкоуровневая таблица) |
| `WorldToScreen(vec)` | `game.world_to_screen` |
| `trace` (глобальная таблица) | `trace.line(from, to, [skip])`, `trace.bullet(from, to, [skip])` — НЕ `game.trace.line` (game.trace — функция) |
| `engine` / `entity` (глобалы) | алиас `game` — буквально ОДНА и та же таблица: compat-функции `entity` (get_prop/set_prop/get_bounding_box/…) кладутся в неё и видны в обоих именах |
| `FilesExists` | алиас `utils.FileExists` |
| `on_shutdown()` | алиас `__shutdown()` (Neverlose-стиль) |
| `globals.mapname` | имя карты ("" вне игры) |
| `globals.lastoutgoingcommand` | tick последней отправленной команды (0 вне createmove) |

---

## Compat-глобалы

Gamesense/Fatality-скрипты работают из коробки:

- `client.*` — log, screen_size, trace_line/bullet, eye_position, camera_angles,
  get_player_name, latency, timestamp, get_username, get_cheat_name, cmd/exec,
  set_event_callback/unset_event_callback, userid_to_entindex;
- `renderer.*` — text/rect/rect_filled/gradient/line/circle/circle_outline/
  triangle/world_to_screen/measure_text/indicator/load_svg/load_png/texture/create_font;
- `globals.*` — realtime, curtime, tickcount, frametime() (frame-independent),
  framecount, absoluteframetime, tickinterval, maxplayers;
- `entity.*` — get_local_player, get_players(alive), get_prop/set_prop (реальные),
  get_bounding_box, get_classname, is_dormant, get_weapon, hitbox_position;
- `callbacks.register/unregister(name, fn)`, `plist.set/get/save` (per-player KV);
- `cvar`, `json`, `clipboard`, `bit32`, `FileExists`, `database` (заглушка →
  используйте `utils.DbSave/DbLoad`);
- `mods.events` — менеджер любых событий (= events).

## FFI (x64: __thiscall НЕ существует)

LuaJIT FFI доступен по умолчанию: `ffi.cdef`, `ffi.cast`, ...
Предобъявлены типы `Vector3/QAngle/Vector2/GetInterfaceFn`.
`cheat.enable_ffi()` — вернуть ffi, если среда его вырезала.

Два хелпера — разница в том, откуда берётся адрес:

```lua
ffi.CallFn(addr, 'int(__cdecl*)(int, int)', 2, 3)              -- сырой адрес: вызываем как есть
ffi.CallVFunc(ptr, 6, 'int(__fastcall*)(void*, float)', 1.5)  -- метод vtable[6] объекта ptr
```

**Конвенция self — вариант B (self подставляется сам):** `CallVFunc(ptr, idx, sig, ...)`
достаёт `vtable[idx]`, приводит к `sig` и вызывает как `fn(ptr, ...)` — `ptr`
уже ушёл первым аргументом. В сигнатуре `self` описан (`void*` первым), но при
вызове его НЕ дублируют: `..., 1.5`, а не `..., ptr, 1.5` (иначе `1.5` уедет во
второй параметр, а `ptr` продублируется). `CallFn` ничего не подставляет —
что передали, то и ушло.

**Важно (x64):** соглашения `__thiscall` на x64 НЕ существует (только x86).
spype — x64-процесс: пишите `'__fastcall*'` с `self` (`void*`) первым параметром:

```lua
local dev = draw.GetDevice()  -- lightuserdata, адрес ID3D11Device*
-- вызвать метод vtable[8] с (void* self, int): self подставится сам
local x = ffi.CallVFunc(dev, 8, 'int(__fastcall*)(void*, int)', 42)
```

**Граница гарантий:** всё, что идёт через `memory.*` и `game.*`, проверяет
адрес и возвращает `nil/false`. `ffi.CallFn`/`ffi.CallVFunc` — прямой cast и
вызов: адрес обязан быть валидным, иначе AV (его поймает SEH тика, но
состояние скрипта после такого вызова не гарантировано).

Прочее: `ffi.HandleToRef(h)` → index|nil (CHandle → entity index), 
`utils.find_pattern/find_export/get_unix_time/fnv1a/murmur2/base64_encode/base64_decode/
string_to_array/array_to_string/clipboard_get/clipboard_set/file_read/file_write/
file_exists/file_list/file_remove/file_rename/file_create_directories/json_encode/
json_decode` — snake_case алиасы `utils.FindPattern` и т.д. (оба стиля работают).

### ffi.VFuncPtr — готовая обёртка vtable-метода

`ffi.VFuncPtr(ptr, idx)` → `{addr = адрес_функции, call = function(sig, ...)}` | nil.
Адрес читается pcall'ом (кривой ptr → nil вместо падения), `call` подставляет
`self` сам — то же соглашение, что CallVFunc:

```lua
local net = net.channel()
local v = ffi.VFuncPtr(net, 10)             -- CNetChannel vtable[10] = get latency
if v then print(v:call('float(__fastcall*)(void*)')) end
```

Это НЕ хук (перехват с вызовом оригинала из Lua.SEH невозможен) — просто
безопасный дескриптор метода. Настоящие перехваты логики чита — `cheat.hook`
(точки пайплайна).

Ручной разбор vtable (`memory.read_u64(vt + idx*8)`) нужен только для
нестандартных случаев; обычно хватает `CallVFunc` и `memory.get_vfunc(ptr, idx)`.

## Песочница

Стандартная библиотека урезана: `load/loadstring/dofile/loadfile`,
`io.open/popen/tmpfile`, `os.execute/remove/rename/exit/getenv/setlocale/tmpname`,
`package.loadlib`, `debug.*` (кроме `traceback` и нашего `debug.stack`),
`string.dump`. Наши API вместо этого: файлы — `utils.File*`, память —
`memory.*`, паттерны — `utils.FindPattern`.

**Осознанные исключения** (важно не путать с «вырезано»): песочница обнуляет
`require` и `ffi`, но слой биндингов ставит их обратно:

- `require('lib.vector')` — свой загрузчик, читает **только** модули из
  `scripts/` (см. §19); стандартный файловый `require` недоступен;
- `ffi` — настоящий LuaJIT FFI, включён по умолчанию (как в Fatality). Это
  escape hatch: `ffi.CallFn/CallVFunc` не возвращают `nil` при плохом адресе,
  а выполняют вызов; ошибка ловится внешним SEH тика, но гарантий
  «unsafe → nil/false» здесь нет.

## Render contract (что гарантировано при рисовании)

**Где можно рисовать 2D:**

| Событие | Стейты | Что видите |
|---|---|---|
| `present_queue` / `draw` / `on_paint` | после нашего end_frame | чистый RTV = backbuffer, свежий scissor, viewport на весь экран |
| `player_info_post` | в середине ESP-пайплайна | чистый RTV, но draw-пайплан живой: рисуем как поверх ESP-игрока |
| `render_start_post` | после построения сцены | предрендер-стейты: только scene.* (3D) — 2D-рендер создавать НЕЛЬЗЯ |
| `createmove` / `frame_stage_notify` | НЕ рендер | draw.* НЕ работает (результат не виден; ошибки нет) |

**Клип-стейт:**
- `draw.push_clip(x,y,w,h)` / `draw.pop_clip()` — стек клипов; пересечение вложенных клипов — как в xdraw (`r.left=max`, `r.right=min`)
- `draw.with_clip(x,y,w,h,fn)` — RAII: гарантированный pop даже если fn кинет ошибку
- `pop_clip` на пустом стеке — без ошибки (pop пустого стека = no-op)
- **Утечка при push без pop:** стейт клипов — render-side (не script-side state). В конце каждого кадра end_frame() очищает clip_stack, так что push без pop НЕ утекает в следующий кадр — текут (лишние отсечения) только остаток этого же кадра (риск залезть на соседний UI). Используйте with_clip, чтобы забыть про pop.

**scene.* (3D):** рисуется в конце present (после end_frame), честная проекция + клип по near-plane. Из `render_start_post` работать тоже — это рендер-поток. Не вызывайте scene.* из createmove / frame_stage_notify — проекция матрицы кадра там недоступна.

## UI: три слоя (канон → alias)

Внимание: **это три разных API, не смешивайте в одном скрипте.**

| Стиль | Формат | Что есть | Для кого |
|---|---|---|---|
| **gui.*** (КАНОН) | id-объект + методы `:get_value()`, `:add_callback(fn)` | `gui.checkbox(id, def)`, `gui.slider(id, lo, hi, def)`, `gui.MakeControlEasy(...)`, `gui.GetThemeColors()` | новые скрипты spype |
| **ui.*** (процедурный) | id-строка через `ui.get(id)` | `ui.checkbox(id, label, def)`, `ui.slider_int(...)`, `ui.combo(...)`, `ui.set_visible/get/set_label` | легаси-скрипты, совместимый уровень |
| **ui.new_*** (Gamesense-compat) | id-строка через `ui.get(ui_id)` (единый геттер; `ui.read` НЕ существует) | `ui.new_checkbox(group, name)`, `ui.reference(group, tab, name)` | Gamesense-скрипты, запускаются из коробки |

Для новых скриптов spype — **только gui.*** (полные объекты, контрол перестроенный,
мета-доступ). Появившихся ранее ui.* хватает для существующих скриптов, ui.new_* —
разблокирует совместимость с огромной базой готовых Gamesense-скриптов.

## script.bus / script.store

Скрипты общаются БЕЗ глобалов (у каждого изолированный lua_State):

```lua
-- script A:
local bus = script.bus('my_mod')
bus:on('reload', function(data, from)
    print(from, 'reload', data.tick)
end)

bus:emit('reload', { tick = game.global_vars.tick_count })  -- доставится на следующий tick()

-- script.store: живое общее KV (не persist — для persist есть cheat.kv_*)
script.store.set('shared_flag', true)
-- ... в другом скрипте:
local v = script.store.get('shared_flag')
```

- `bus:emit(...)` возвращает число подписчиков (0 = никто не слушает)
- доставка в render-потоке — колбэк получает `(data, from)` (from = имя скрипта-отправителя)
- при выгрузке скрипта подписки снимаются автоматически
- имя шины и события — строки ≤64 символов

**Сериализация payload (trivial-JSON):**
- поддерживаются: таблицы (массивы и словари со строковыми ключами), числа, строки, boolean, nil
- `nil` кодируется как `null` и декодируется обратно в `nil`
- функции, userdata, thread, циклические таблицы и нестроковые ключи НЕ кодируются:
  `bus:emit` в этом случае доставляет получателю `nil`, а `script.store.set`
  возвращает `false` и ничего не пишет
- лимиты: глубина вложенности ≤8, размер ≤64 КБ (превышение ведёт себя как некодируемое значение)

**script.store:**
- `set(key, value)` → bool; `get(key)` → значение или nil
- `delete(key)` → bool (true — ключ был); `list()` → `{ключи...}`
- числа/строки/boolean/nil возвращаются как есть, а ТАБЛИЦЫ `get` отдаёт
  JSON-строкой — декодируйте через `utils.JsonDecode`
- ключи — строки ≤128 символов; store не переживает перезапуск игры
  (для persist — `cheat.kv_*`)

## lagcomp.* — данные отката для скриптов

Read-only вид на тот же lagcomp, которым стреляет ragebot (основание для backtrack-ESP):

```lua
local info = lagcomp.info()
print(info.tracked_targets, info.valid_records, info.available_ticks)
-- экстраполяция вообще работает? extrap_targets > 0 — да:
print(info.extrap_targets, info.extrap_ticks, info.extrap_reason)

for _, p in ipairs(game.players()) do
    if p.is_enemy then
        for _, r in ipairs(lagcomp.records(p)) do
            -- r = {tick, sim_time, origin (vec3), extrapolated, extrap_ticks,
            --      bones[27] = {position (vec3), rotation ({x,y,z,w})}}
            -- backtrack-ESP: скелет/бокс на прошлом тике — из r.bones
            draw.world_text(r.origin, tostring(r.tick), 0xFFFFFFFF)
        end
    end
end
```

- `lagcomp.info()` → `{tracked_targets, valid_records, available_ticks, extrap_targets, extrap_ticks, extrap_reason}`
  (бегущих счётчиков нет — lifetime-числа убраны, значения ограничены текущим
  раундом; счётчики сбрасываются на смене карты/раунда)
- `lagcomp.records(pawn)` → массив `{tick, sim_time, origin, extrapolated, extrap_ticks, bones}`:
  **bones — реальный скелет рекорда** (27 костей: `position` vec3 + `rotation`
  {x,y,z,w} на каждом) — backtrack-ESP рисует бокс/скелет на прошлом тике,
  trail/точки — из origin. pawn — entity-объект или число-указатель
  (как `rage.get_target().pawn`); нет записей — пустая таблица, кривой
  указатель — nil
- `lagcomp.interpolate(pawn, tick)` → `{origin}` | nil — линейная интерполяция
  между соседними валидными рекордами; тик вне рекордов (будущее) — nil
- `lagcomp.restore(cmd, tick)` из скриптов НЕТ — apply/restore живёт только
  на командном потоке чита (рендер-поток не может временно откатывать records
  безопасно); для трейсов по прошлым позам используйте `lagcomp.records`
- `extrap_reason`: 0 — предсказание построено, 1 — поза свежая (lagcomp
  достаточно), 2 — поза слишком старая (ghost guard), 3 — поза актуальна
  для этого тика, 4 — нет записей, 5 — цель разворачивается (velocity-only
  модель), 6 — цель стоит на месте, 7 — нет scene node
- данные — потокобезопасная копия (снапшот); живое хранилище игрового потока скриптам недоступно
- краткая сводка без рекордов — `rage.get_lagcomp()`

## particles.* / panorama.* (Блок 12, реверс-слой)

Реальные нативные вызовы по сигнатурам, которые уже используются фичами
чита (weather/impacts — частицы, scoreboard_weapons — Panorama). Все функции
безопасны: при недоступности паттернов/менеджера возвращают `nil`/`false`,
при кривом адресе — SEH, игра не падает.

```lua
-- particles.*
if particles.available() then
    local h = particles.create("particles/rain_fx/rain.vpcf", 100, 200, 0)
    -- или particles.create("particles/...vpcf") без позиции
    if h then
        particles.set_control_point(h, 0, 100, 200, 0)   -- CP0 = позиция
        particles.set_control_point(h, 16, 255, 0, 0)    -- CP16 = цвет (0..255)
        particles.set_transform(h, 100, 200, 0)           -- позиция+кватернион
        -- particles.destroy(h) -- убрать эффект
    end
end

-- panorama.* — JS в Panorama-движке HUD
if panorama.available() then
    panorama.Eval([[GameInterfaceAPI.ConsoleCommand('cl_drawhud 0')]])
    panorama.Eval('$.Msg("hello from lua")')
    -- одноразовый JS работает любой, включая события и панели:
    panorama.Eval([[ $.DispatchEvent('HudChatShow', {text='hi'}) ]])
    panorama.Eval([[
        var p = $.CreatePanel('Label', $.GetContextPanel(), 'spype_note');
        p.text = 'hello'; p.style.color = 'red';
    ]])
end

-- game.* алиасы (Fatality-стиль)
local h = game.CreateParticle("particles/embedded/sparks.vpcf", x, y, z)
game.SetParticleControlPoint(h, 0, x, y, z)
game.ReleaseParticle(h)
```

- `particles.create(path [, x, y, z])` → handle|nil; позиция сразу ставится в CP0
- `particles.set_control_point(h, cp, x, y, z|{x,y,z})` → bool
- `particles.set_transform(h, x, y, z [, qx, qy, qz, qw])` → bool
- `particles.destroy(h)` → bool (алиас `particles.release`)
- `particles.available()` → bool
- `panorama.Eval(js)` / `panorama.eval` / `panorama.run_script` → bool
  (лимит 4096 символов, `\r` вырезаются; выполняется как run_script в контексте
  HUD-панели: `GameInterfaceAPI.*`, `$.Msg`, одноразовые `$.DispatchEvent` и
  `$.CreatePanel` работают; обратных вызовов JS→Lua НЕТ — колбэки из JS
  в скрипт не доставляются, см. «Не реализовано»)
- `panorama.available()` → bool

## net.* / voice.* (Блок 12, реальные данные)

```lua
-- net.* — через CNetChannel (тот же, что в lagcomp чита)
local ms = net.latency()          -- RTT в секундах | nil вне игры
local ping = net.engine_latency() -- серверный пинг из m_iPing, сек | nil
local nc  = net.channel()         -- адрес CNetChannel числом | nil
if net.connected() then print(net.tick_rate()) end

-- voice.say — цветная строка в HUD-чате (видна только тебе, в сеть НЕ уходит)
voice.say("скрипт загружен")
```

- `net.latency()` → number|nil (сек), алиас `net.Latency`
- `net.engine_latency()` → number|nil (сек, из CCSPlayerController.m_iPing)
- `net.channel()` → integer|nil (адрес CNetChannel числом: работает и арифметика
  `nc + 0x10` для memory.*, и `ffi.cast('void*', nc)` — число приводится к указателю)
- `net.connected()` → bool, `net.tick_rate()` → int
- `net.server_address()` → string|nil (`"144.126.140.60:27015"`-формат; эмпирическое
  чтение netadr из CNetChan с кэшем офсета, всё через safe_read)
- `net.steam_id()` → number|nil (SteamID64 из SteamAPI; < 2^53 — без потери точности)
- `voice.say(text)` → bool; `voice.available()` → bool

Реальные client.* (перекрывают старые заглушки):
- `client.get_username()` — настоящее имя локального игрока (m_sSanitizedPlayerName)
- `client.latency()` — алиас `net.latency()` (сек RTT)
- `client.color_log(r, g, b, alpha, text)` — цветная строка в консоль чита
- `client.delay(...)` — заглушка (false); используйте `Delay(sec, fn)`
- `client.userid_to_entindex` — по-прежнему nil (в CS2 userid нигде не хранится на клиенте; полный перебор невозможен без server-side данных)

## Жизненный цикл скрипта

```
┌─ галка enabled в меню / cheat.reload_script
│
├─ 1. создаётся СВОЙ lua_State (изолирован от других скриптов)
├─ 2. выполняется код верхнего уровня ОДИН раз
│      (тут вы определяете on_paint/events/хуки/константы)
├─ 3. каждый кадр рендера: tick() →
│        pump_binds (binds.* колбэки) →
│        due_tasks (utils.sleep/utils.thread) →
│        invoke_on_paint (ваша on_paint)
├─ 4. события (createmove, present, ...) дергают события и хуки
│      в момент, когда движок их вызывает (не «по кадрам»)
└─ 5. выгрузка:
         галка снята → events.unload:add() срабатывает →
         __shutdown() → lua_State закрывается
          ОШИБКА в скрипте → событие unload НЕ вызывается,
          __shutdown НЕ зовётся (см. «Гарантии» ниже), скрипт останавливается
```

**Гарантии:**
- `events.unload:add(fn)` — гарантированно вызывается ПЕРЕД `__shutdown()`;
- при ошибке — ни unload, ни shutdown (см. «Ошибки» в начале), НО `events.error`
  вызывается ДО остановки скрипта — подписка `events.error:add(fn)` получает
  `(error_text, event_name)` и может залогировать сбой сам;
- НО: все мировые мутации (world.set_fog/skybox/wind/weather/lighting/
  world_color/bloom/gamma, world.add_light, input.set_blocked) теперь откатываются
  автоматически при ЛЮБОМ завершении скрипта — вручную и при ошибке (undo-реестр
  рантайма, LIFO). Сигнатура: сеттер снимает снапшот до записи; stop_script/
  падение/выгрузка возвращают предыдущие значения. Скрипт больше не может
  оставить игру «в тумане навсегда»;
- `cheat.reload_script(name)` = stop_script + start_script (тот же порядок шагов 1-2-5);
- KV (`cheat.kv_*`) и ui-переменные переживают reload и перезапуск игры;
- локальные переменные скрипта НЕ сохраняются между запусками.

**Песочница**: бюджет времени и памяти: если скрипт съедает
больше `cheat.set_script_budget(ms)` (по умолчанию 100 ms за кадр) или
`cheat.set_memory_limit_mb(mb)` (64 МБ) — скрипт останавливается с ошибкой
"script exceeded time/memory budget". Не ловушка — защита от зависания игры.

## Типичные ошибки

| Симптом | Причина | Фикс |
|---|---|---|
| Внезапный nil у игрока, который только что был | сохранили entity-объект в глобал, сущность умерла | проверяйте `p.valid` / храните handle + `game.entity_by_handle` |
| `events.player_hurtt:add(...)` тихо не работает | опечатка: имя не матчится с реальным событием | `events.list()` покажет все доступные имена |
| Событие срабатывает не каждый кадр, а хаотично | подписались на `player_hurt` (только факты) вместо `draw`/`present_queue` | рисуйте/постоянные операции — из `draw`/`present_queue` |
| script exceeded time budget | скрипт перескочил 100 ms за кадр | кэшируйте, делайте работу реже, увеличьте `cheat.set_script_budget()` |
| memory.* вернул 0, но адрес точно верный | очень часто неверный смещение после нового обновления игры | проверьте другой известный адрес или пересчитайте офсет |
| coroutine error: yield вне spawn | `utils.yield()`/`wait_frames` вне `utils.spawn` | оберните в `utils.spawn(function() ... end)` |
| `game.cmd()` nil | вызов вне createmove | вызывайте в колбэке `events.create_move` |
| ESP не рисуется | draw.* вызов вне present_queue | перенесите в `events.present_queue:add(fn)` |
| Двойная обработка события | один и тот же хук подписан и через `cheat.hook`, и через `cheat.register_callback` | оставьте одно |
| `http.GetAsync` колбэк никогда не вызывается | `res` не объявили, замыкание пишет в nil | `local res = nil` перед вызовом, или используйте `utils.spawn` + `yield` |
| script_name() вернул пустую строку | вызов из функции/колбэка на «внутреннем» стеке, не с верхнего уровня | сохраняйте `local NAME = cheat.script_name()` у верхнего уровня скрипта и используйте её |

## Fatality-паритет: новое в API (октябрь)

Реальные C++-биндинги, добавленные в `lua_bindings_fatality.cpp` /
`lua_bindings_entity.cpp` (всё — рабочее, не заглушки):

### events.event: GetController / GetPawnFromId

```lua
local my_kill = function(e)
    local attacker = e:GetController("attacker")   -- CCSPlayerController | nil
    local victim   = e:GetPawnFromId("userid")     -- C_CSPlayerPawn     | nil
    local assister = e:GetController("assister")   -- тоже по userid-полю
end
```

- Аргумент — ЛЮБОЕ поле события, в котором лежит userid: "attacker",
  "userid", "assister" (как в Fatality/PND).
- Возвращает standard-объект Entity spype (те же методы/поля, что у
  `entities.get_local_pawn()`: `.alive`, `.is_valid`, `.controller`,
  `:get_netvar("m_...")`, `:netvarv("m_...")`, `.health`, `.origin` и т.д.).
  Маппинг userid→контроллер — вызов движкового
  `game_event_get_controller`/`game_event_get_pawn` (SEH-guard, fallback
  controller + m_hPawn): перебора энтитей из Lua нет, 20+ подписок в
  кадре не тормозят.
- Типовые применения: hitsounds, trash-talk, hit markers, aimpunch,
  scoreboard/reveal chat — одна строка вместо ручных таблиц.

### game.engine:ClientCmd и client.client_cmd — РЕАЛЬНАЯ отправка

```lua
game.engine:ClientCmd("say_team ga")   -- aliases: ClientCmd / client_cmd / exec
client.client_cmd("cl_showfps 1")       -- aliases: client_cmd / cmd / exec
```

- Оба идут по одному настоящему пути:
  `engine_client_cmd(source2engine_to_client, 0, cmd, 0x7ffef001)` — тот же
  вызов, что и внутренние фичи чата/команд чита.
- Пустая строка или отсутствие паттерна — не отправляется, в журнал пишется
  `[client_cmd] dropped: ...`.
- Вызывайте из коллбеков (events.event / create_move / on_paint), а не из
  «фоновых» нитей — путь проходит через engine-интерфейс, который живёт на
  игровом потоке.

### Нетвар как объект: :Get().value (PND-стиль)

```lua
local next_fire = weapon:netvarv("m_nNextPrimaryAttackTick")
print(next_fire.value)   -- снимок значения на момент создания объекта
next_fire:Get()          -- перечитывает из живой памяти и возвращает объект
print(next_fire:Get().value)  -- всегда свежее значение (PND: :Get().value)
```

- `entity:netvarv("m_...")` возвращает объект `{ value = ..., Get/get }`.
  `.value` — снимок; `:Get()` обновляет `.value` чтением живой памяти и
  возвращает сам объект, поэтому `x:Get().value` — это «прочитать прямо
  сейчас».
- Тип определяется по имени нетвара: int32 / float / bool / vec3;
  vec3 возвращается как vec-объект (`.x/.y/.z`).

### mods.penetration / mods.events — реальные (не заглушки)

```lua
local dmg = mods.penetration.ScaleDamage(100.0, weapon_ptr, 1, pawn_ptr) -- hg: 1=head
mods.penetration.FireBullet(...)        -- алиас на настоящий trace.bullet-путь
mods.events:AddListener("player_hurt", function(e) ... end, priority)
```

- `ScaleDamage`/`FireBullet` живут на боевой пенетрации чита
  (`features::combat::penetration`): броня/хитгруппа/шлем/команда считаются
  тем же кодом, что и для реального выстрела ragebot'а.
- `mods.events:AddListener` — другой синтаксис подписки: внутри
  вызывает `events[name]:add(fn[, prio])`.

### gui.Group / GetMainWindow():AddTab / GetHotkeyList (Fatality-стиль)

```lua
local grp = gui.Group("my group")            -- layout-объект
local tab = gui.GetMainWindow():AddTab("my tab")
grp:add(gui.checkbox("flag", true))
grp:set_visible(false); print(grp:IsVisible(), grp:GetIsVisible())
grp:find("flag"); grp:get_parent()

for _, kb in ipairs(gui.GetHotkeyList()) do
    print(kb:GetLabel(), kb:GetHotkeyState(), kb:Cast():GetValue():Get())
end
```

- Group и AddTab возвращают один и тот же layout-объект: методы
  `add/find/remove/set_visible/IsVisible/GetIsVisible/get_parent/cast`.
- GetHotkeyList отдаёт кейбинды ТЕКУЩЕГО скрипта; контролы несут
  Fatality-алиасы: `.Cast()`, `.GetHotkeyState()`, `.GetLabel()`,
  `.GetValue():Get()`. `.parent` у корневых контролов nil (обход вверх
  до nil — как в Fatality).

### draw.adapter:GetBackBuffer — статус

Swapchain hook для Menu-Effect шейдеров (Blur/Pixelation/Starfield) не
реализован: реверс swapchain-обёртки — отдельная большая задача, а эффекты
использует меньшинство скриптов. Чекбокс в скрипте работает, шейдер
безопасно пропускается.

## Movement-ядро

Блок данных движения, который нужен своим стрейферам, jumpbug/edgebug и любым
связкам movement+rage. Всё читает **prestate текущей команды** — снимок
состояния ДО применения движения (`systems::g_prediction.capture_prestate`),
поэтому значения согласованы с той командой, которую скрипт собирается править.

### movement.*

| Функция | Возврат |
|---|---|
| `movement.get_velocity()` / `get_networked_velocity()` | Vector3 — сетевая скорость (та, что видит сервер) |
| `movement.get_predicted_velocity()` | Vector3 — предсказанная скорость |
| `movement.get_speed()` | number — горизонтальная скорость (u/s) |
| `movement.get_origin()` | Vector3 — живой origin пешки |
| `movement.get_networked_origin()` | Vector3 — сетевой origin |
| `movement.get_flags()` | integer — FL_* биты (на земле = бит 0) |
| `movement.is_on_ground()` / `is_in_air()` | bool |
| `movement.get_move_type()` | integer — m_MoveType (0, если поле исчезло из схемы) |
| `movement.get_max_speed()` | number — m_flMaxspeed текущего оружия |
| `movement.get_accelerate()` / `get_air_accelerate()` | number — sv_accelerate / sv_airaccelerate |
| `movement.get_air_max_wishspeed()` | number — sv_air_max_wishspeed (обычно 30) |
| `movement.get_friction()` / `get_stopspeed()` / `get_gravity()` | number — конвары |
| `movement.get_surface_friction()` | number — поверхность под ногами (0..1) |
| `movement.get_stamina()` | number |
| `movement.get_velocity_yaw()` | number — yaw направления скорости, 0 если стоим |
| `movement.get_strafe_side()` | integer — -1/0/+1: куда стрейфить относительно взгляда |
| `movement.get_ideal_strafe_delta([speed])` | number — оптимальный угол между скоростью и wishdir (90° на малой, ~acos(30/speed) на высокой) |
| `movement.circle_yaw(yaw, dir, [speed])` | number — следующий yaw кругового стрейфа (`dir` = -1/+1 или `"left"`/`"right"`) |

### game.prediction.* (алиас: глобал `prediction`)

Тот же источник данных, но с именами из плана: `is_active()`, `get_origin()`,
`get_velocity()`, `get_flags()`, `get_move_type()`, `max_speed()`,
`air_accelerate()`, `friction()`, `gravity()`, `stamina()`, `is_on_ground()`,
`is_in_air()`, `get_speed()`.

`is_active()` = true, пока идёт create_move (есть валидный usercmd).

**Чего НЕТ и почему:** `prediction.start()/end()/simulate()` не реализованы —
честное предсказание на N тиков требует вызова игрового `RunCommand` на
локальной пешке. Это делается в C++ (`systems::g_prediction.simulate_ticks`),
но наружу в Lua не отдано: ошибка в таком вызове — падение в игровом потоке,
а не ошибка скрипта. Если нужен прогноз — используйте `movement.*` (prestate)
и считайте позицию сами (гравитация/фрикция известны через конвары).

### cmd — раздельные оси

```lua
cmd:get_forwardmove() / cmd:set_forwardmove(f)
cmd:get_sidemove()    / cmd:set_sidemove(s)
cmd:get_upmove()      / cmd:set_upmove(u)
cmd:get_impulse()     / cmd:set_impulse(i)
cmd:get_weapon_select()
```

`set_move(fwd, side)` остаётся — это просто обе оси сразу.

### Хуки движения

| Хук | Аргументы | `return false` |
|---|---|---|
| `movement.autostrafer` | `speed, in_air` | встроенный стрейфер не работает эту команду |
| `movement.jumpbug` | `speed, in_air` | встроенный jumpbug пропускается |
| `movement.edgebug` | `speed, in_air` | встроенный edgebug пропускается |
| `movement.pre` / `movement.post` | — | — (уже были: правка команды до/после фич) |

Любой другой возврат (`nil`, число, `true`) оставляет встроенное поведение.
Эталон — `bin/scripts/example_autostrafe.lua` (полная замена стрейфера).

### rage.get_scan_data()

```lua
for i, pt in ipairs(rage.get_scan_data()) do
    -- pt.pos = Vector3, pt.hitbox = индекс хитбокса, pt.center = bool
end
```

Точки последнего скана (те же, что у встроенного debug-оверлея). Копия под
мьютексом — можно хранить между тиками. Эталон:
`bin/scripts/example_scan_data.lua`.

### debug.*

| Функция | Описание |
|---|---|
| `debug.time_start(label)` | запомнить точку старта |
| `debug.time_end(label)` | вернуть прошедшие мс и записать в таблицу |
| `debug.get_times()` | `{ [label] = { last=ms, last_ms=ms, count=n } }` |
| `debug.get_memory_usage()` | байты Lua-хипа (`lua_gc(GCCOUNT/GCCOUNTB)`) |
| `debug.trace(msg)` | строка в консоль чита |

### legit.*

`get/set_enabled`, `get/set_fov`, `get/set_smooth`, `get/set_rcs`,
`get/set_triggerbot`, `get/set_trigger_delay`, `get/set_trigger_hitchance`,
`get/set_autowall`, `get/set_min_damage`, `has_target()`.
Работают с группой оружия, которую держит игрок (как меню).

### Типичные ошибки

- `cmd:set_*` вне `create_move` — `game.cmd()` вернёт `nil`; проверяйте его.
- Скрипт, вернувший `false` из `movement.autostrafer`, ОБЯЗАН сам выставить
  `forwardmove/sidemove` и yaw — иначе игрок просто перестанет ускоряться.
- Писать `set_view_angles` в movement-хуке и ждать, что anti-aim останется
  нетронутым, нельзя: хук выполняется после AA и перекрывает его позу.

## Ragebot: мультипоинты

### Как это работает (без магии)

Для каждой включённой группы хитбоксов скан делает так:

1. **Центр** — всегда: середина хитбокса, повёрнутая костью. Это самая
   надёжная точка, она даёт правильную хитгруппу.
2. **Краевые точки** — только если для этой группы включён `multipoints`.
   Они сидят на **поверхности** капсулы/бокса (реальный ray-vs-capsule,
   а не «радиус на глаз») и тянутся к центру на величину `point scale`.
3. **Dynamic point scale** уменьшает глубину края, когда конус разброса
   на дистанции шире хитбокса: иначе точка «висит» снаружи и её просто
   не достать.

Две новые ручки задают предел глубины отдельно:

| Настройка | Что значит | Дефолт |
|---|---|---|
| `point scale` | общая глубина края (0–100% от поверхности) | 85% |
| `head point scale` | предел для **головы** | 55% |
| `body point scale` | предел для тела/конечностей | 90% |

Меньше процент — точка глубже внутрь: надёжнее хитгруппа, уже покрытие.
Больше — ближе к краю: ловит торчащую голову/плечо, но сервер может
записать соседнюю хитгруппу (классические «нанёс 71–91, а он жив»).

`head wide ring` добавляет голове 4 диагональные точки — кольцо из 8
направлений вместо 4 осевых. Это +4 капсульные пробы только на голову и
заметно повышает шанс зацепить частично открытую голову на дистанции.

`debug multipoints` рисует **ровно то, что скан реально пробовал**: крупная
точка (alpha 255) = центр, мелкая (alpha 160) = край. Если у группы выключены
мультипоинты, у неё будет только крупная точка — это и есть ответ на вопрос
«почему показывает все хитбоксы»: он показывает все *просканированные точки*,
а не «выбранные хитбоксы».

### Настройки

```lua
rage.get_multipoints( group )        -- 0..5: head/chest/stomach/arms/legs/paws
rage.set_multipoints( group, true )
rage.get_head_pointscale( )          -- 20..100
rage.set_head_pointscale( 70 )
rage.get_body_pointscale( )
rage.set_body_pointscale( 90 )
rage.get_head_wide_ring( )
rage.set_head_wide_ring( false )
```

Группы те же, что у `hitboxes` в меню — можно сделать скрипт, который
включает мультипоинты только на голове на пистолетном раунде и отключает
на эко.

### Полный Anti-Aim из скрипта

`cheat.hook("antiaim.pre", function(next, yaw, pitch) ... end)` вызывается
до встроенного AA; `return false` — встроенный AA на этой команде не пишет
ничего, и скрипт сам формирует позу (включая subtick-углы). В отличие от
`antiaim.override`, который только подменяет yaw/pitch/desync уже посчитанной
позы, `antiaim.pre` отдаёт всю логику целиком.

## Ввод, оружие, гранаты

Эти три таблицы закрывают то, чего раньше не было в Lua вообще, — без них
свои nospread/recoil-control, mouse-based AA и гранатовая логика упирались
в догадки.

### input.* — сырой ввод

| Функция | Возврат |
|---|---|
| `input.get_mouse_delta()` | `dx, dy` — дельта мыши, которую сервер видит в этой команде (`mousedx/mousedy`) |
| `input.get_scroll_delta()` | всегда `0`: в CS2 колесо потребляется input-системой до сборки usercmd, честного источника нет (оставлено для совместимости) |
| `input.get_cursor_pos()` / `set_cursor_pos(x, y)` | экранные координаты курсора |
| `input.get_sensitivity()` | `sensitivity` из конваров |
| `input.is_key_down(vk)` / `was_key_pressed(vk)` | состояние клавиши (VK_*) |

`get_mouse_delta` — это ровно та дельта, которую usercmd несёт на сервер.
Внутри `events.draw` она равна последней собранной команде; вне игры — 0.
Для mouse-AA: пишите свой yaw в `antiaim.override`, а дельту читайте здесь,
чтобы повторять человеческий input, а не генерировать телепорт.

### weapon.* — текущее оружие

`weapon.get()` возвращает таблицу (или `nil` вне игры):

| Поле | Что это |
|---|---|
| `ptr`, `vdata` | указатели оружия и CCSWeaponBaseVData |
| `id`, `type`, `bullets` | item definition index, тип (`cstypes::weapon_type`), число пуль за выстрел |
| `clip`/`ammo`, `reserve`, `max_clip`, `reloading` | патроны |
| `damage`, `penetration`, `range`, `range_modifier`, `armor_ratio`, `headshot_multiplier` | таблица оружия |
| `recoil_index`, `inaccuracy`, `spread`, `max_speed` | текущее состояние разброса |
| `next_primary_tick`, `next_secondary_tick`, `tick_base`, `can_fire` | тайминги атаки |

Поле `inaccuracy` — живой `m_fAccuracyPenalty` через движковый расчёт, а не
«примерно»: по нему можно строить свой crosshair-индикатор и свои гейты.

```lua
local w = weapon.get()
if w and w.can_fire and w.inaccuracy < 0.002 then
    -- оружие реально готово и точное
end
```

### grenade.* — предсказание гранаты

```lua
grenade.predict(origin, velocity, {
    gravity = 800, radius = 2, bounce = 0.45,
    friction = 0.6, max_time = 3.0, step = 1/64, max_points = 256,
})
-- -> { points = { {pos={x,y,z}, time=..., speed=...}, ... },
--      land = {x,y,z}, land_time = 1.9, bounces = 2,
--      distance = 700, count = 120 }
```

Траектория считается **реальными трейсами мира** (`trace_sphere`), а не
только формулой: отскоки с потерей скорости, трение, пол под ногами.
Отсюда можно рисовать свой grenade prediction через `draw.scene`/`draw.polyline`
и считать, куда упадёт смок, до броска.

`grenade.get_held()` -> `{ id, is_grenade, time }` или `nil`.

## spype sync — бейджи, общая библиотека и облако

Сервис: Cloudflare Workers + D1, страница и API в одном воркере.
Бейдж виден над ником в мире и в Tab-скорборде (через панораму).

### Что делает клиент

`bin/scripts/spype_sync.lua` — готовый скрипт, включите его галкой в меню:

1. создаёт локальный ключ профиля (`scripts/spype_sync_key.txt`);
2. раз в 20 секунд шлёт heartbeat с xuid/ником и списком xuid лобби;
3. рисует бейдж над головой подтверждённых игроков;
4. пушит бейджи в Tab-скорборд (`SClient.setBadges` в
   `scoreboard_weapons.cpp`, панель `spype-sync-badge-<xuid>`);
5. отдаёт таблицу `sync.*`.

Endpoint по умолчанию — `https://spype.pages.dev`; переопределяется файлом
`scripts/spype_sync_endpoint.txt` (по одному адресу в строке) или
`sync.set_endpoint(url)`. Если адрес не отвечает (например, его режет
провайдер по SNI), клиент сам переходит к следующему из списка.

### API

| Функция | Что делает |
|---|---|
| `sync.is_verified(xuid)` | true, если игрок зарегистрирован в sync |
| `sync.badge(xuid)` / `sync.player_name(xuid)` | бейдж/ник из профиля |
| `sync.online()` | сколько профилей вернул последний heartbeat |
| `sync.loadout(xuid)` / `sync.set_loadout(json)` | обмен лоадаутом (скины) через профиль |
| `sync.endpoint()` / `sync.endpoints()` | текущий адрес / весь список запасных |
| `sync.set_endpoint(url)` | поставить адрес первым (старые остаются запасными) |
| `sync.add_endpoint(url)` | добавить запасной адрес |
| `sync.key()` / `sync.xuid()` / `sync.name()` | локальный ключ, steam id, ник |
| `sync.profiles()` | таблица профилей `xuid -> {name, badge, loadout}` |
| `sync.ping(cb)` | проверить соединение: `cb(ok, stats_or_err)` |
| `sync.refresh(cb)` | форсировать heartbeat |
| `sync.list(kind, cb)` | список опубликованных луашек/конфигов: `cb(items, err)` |
| `sync.get(id, cb)` | скачать запись: `cb(item, err)` |
| `sync.publish(kind, name, desc, content, cb)` | опубликовать: `cb(ok, id_or_err)` |
| `sync.publish_config(name, cb)` | опубликовать пресет конфига (пусто — текущий) |
| `sync.publish_script(file, title, cb)` | опубликовать файл из `scripts/` |
| `sync.delete(id, cb)` | удалить свою запись: `cb(ok, err)` |
| `sync.last_error()` | текст последней ошибки |

`sync.loadout` — это транспорт, а не магия: сервер хранит JSON, который
вы положили. Применять чужие скины должен скрипт (через `changer`/`entity`),
если/когда такая логика у вас есть.

### Вкладка Cloud в меню

`Scripts → Cloud` (внутри игры) — то же самое, но без скрипта:

- поле адреса + `test` / `save endpoint`, строка статуса (`online`/`offline`);
- фильтр `lua | config | all`, поле имени конфига, выбор скрипта;
- `upload config`, `upload script`, `refresh`;
- список записей: `[kind] name (downloads, xuid)` + кнопки `download` / `delete`.

Конфиг читается через `config::to_json()`, скачанный конфиг применяется
через `config::from_json()`, скачанный скрипт кладётся в папку `scripts/`.

### Сервер

`sync-site/worker.js` (в корне репозитория) — весь бэкенд:

```
GET  /api/health                      -> {ok, version, db, server_time}
GET  /api/endpoint                    -> {ok, endpoint, version}
GET  /api/stats                       -> {ok, items, profiles, online, downloads}
GET  /api/profiles                    -> {ok, users[]}
POST /api/register                    {xuid,name,key,badge}
POST /api/heartbeat                   {xuid,name,key,badge,xuids[],loadout} -> {ok, me, users[], online}
GET  /api/library?kind=lua|config&q=  -> {ok, items[]}
GET  /api/library/:id                 -> {ok, item}
GET  /api/library/:id/raw             -> тело записи (text, ?download=1 — вложение)
POST /api/library                     {xuid,key,kind,name,description,content}
DEL  /api/library/:id                 {xuid,key}
GET  /api/profile/:xuid               -> {ok, user}
GET  /api/docs/lua                    -> LUA_API.md (markdown)
GET  /api/scripts/:name               -> примеры .lua
GET  /api/client                      -> архив клиента
```

Ключ профиля хранится только как SHA-256 и защищает загрузки от подмены
по чужому xuid. Лимиты: луашка 256 КБ, конфиг 512 КБ.

## Ragebot: что изменилось в точности

Эти правки — ответ на сессионные логи, а не «на глаз»:

1. **World-гейт больше не считает живую позу.** Раньше `evaluate_penetration_hitchance`
   на каждый сэмпл запускал движковый bullet-trace, который коллизится с
   **живыми** хитбоксами, тогда как точка прицела принадлежит rewound-записи.
   Отсюда `world gate rejected geo 1.00 world 0.05` и качели 0.03↔1.00 на
   одной и той же чистой линии. Теперь сначала дешёвый `world_clear`
   (только геометрия мира, тела игроков пропускаются) и полная симуляция
   только для реально перекрытых/пробиваемых линий. Плюс это в разы дешевле.
2. **Экстраполяция без лишнего лида.** Прогноз теперь ровно на `age` тиков
   (а не `age + 2`), включается только при чоке ≥ 2 тиков, дрейф-капы
   головы 14u / тела 32u (было 24/48). Класс `extrapolation` (58–146 dmg
   в грудь/голову) — это перелёт по прямой за остановившейся целью.
3. **Shoot position** штампуется как **post-move** глаз: сервер применяет
   движение команды, потом стреляет; прежний pre-move глаз давал остаточный
   `shoot position mismatch` на движущихся выстрелах.
4. **DoubleTap.** `weapon_ready` теперь допускает `next_primary <= client_tick + 1` —
   собственная отдача первого патрона больше не читается как «оружие на
   кулдауне», и второй пульс реально уходит. Убрана «альтернация», которая
   пропускала каждую вторую заряженную попытку.
5. **Recovery-пол.** Жёсткие 80% поверх пользовательского порога убраны:
   recovery использует тот же порог, что и главный гейт.

## Не реализовано (честно)


- `entities.Create/DispatchSpawn/SetModel` — CreateEntityByName живёт на сервере; на клиенте создание сетевых энтити через клиентский интерпретатор не даст предсказуемого результата. Рабочие альтернативы: частицы (см. выше), `patterns::set_player_model` (смена модели существующей энтити),Precache+SetControlPoint.
- `shared.simulate_aim_punch` (внутренний C++) — мёртвый код: живой путь читает аим-панч через `get_aim_punch`. Его зависимость `weapon_recoil_data` сделана ОПЦИОНАЛЬНОЙ: после патча игры с умершей сигнатурой чит теперь грузится (в логе `[warn] optional global unavailable`), страдает только неиспользуемая симуляция отката.
- `net_send/net_recv` хуки CNetChan — OBSERVE-ONLY (реализованы!): `cheat.hook("net_send"/"net_recv")` живёт через pump_net_traffic и доставляет копии трафика; модифицировать пакеты из хука нельзя (нет перехвата записи) — только наблюдение.
- `client.userid_to_entindex` — по-прежнему nil (CS2 не хранит userid на клиенте)
- `client.client_cmd/exec`, `game.engine.client_cmd` — УЖЕ РЕАЛЬНЫЕ:
  отправка через `engine_client_cmd` (см. раздел 38).
- колбэки JS→Lua в panorama (подписка из Lua на события HUD-панелей) — только
  Lua→JS направление (`Eval`); JS→Lua нет проверенного моста.
- `draw.adapter.get_back_buffer/get_shared_texture` — nil (нужен реверс swapchain-обёртки; см. раздел 38).
- `gui.GetHotkeyList` возвращает кейбинды текущего скрипта (не всех контролов меню).
- `utils.FindExport/FindPattern/Clipboard*/File*` работают в папке scripts и в памяти процесса — ограничений «insecure-режима» нет, но и workshop-изоляции тоже нет.
