gmod-rs/docs/PARITY.md
Niko Marmeladkov b991cc59f1 lua: мост к движку — Entity(), ents.* работают с живым миром
Батч 3, шаг 2. Крейт glua не зависит от engine: поведение отдаётся через
трейт Host, реализация которого живёт в клиенте. Иначе был бы цикл
зависимостей и Lua-слой нельзя было бы тестировать в одиночку.

Мир переведён в Rc<RefCell<World>> — скрипт обращается к нему из хуков в
любой момент. Все обращения через try_borrow: пересечение заимствований
даёт panic, а падать из-за скрипта клиент не должен.

Проверено измерением на gm_construct: ents.GetAll() возвращает 1227
сущностей, ents.GetByClass('prop_physics') читает позицию.

Найден и исправлен рассинхрон: move_prop обновлял позу экземпляра пропа,
но не запись в пуле сущностей. Lua читал z = -64, пока физика уже опустила
проп на z = -84 — «данные» и «картинка» разошлись, и заметить это можно
было только глазами. Теперь поза пишется в оба места всегда, даже без
модели: проп без модели всё равно стоит правильно, он просто не рисуется.
Возвращаемое значение move_prop теперь означает именно «геометрия
пересчитана».

После правки Lua читает z = -84.15848, физика даёт -84.15847778320313 —
совпадение до последнего бита.

Тест на паникующий хост: скрипт получает «невалидно» вместо падения.

Тестов 24 в glua, 56 в engine, 23 набора по воркспейсу, clippy чист.
2026-10-13 12:57:01 +03:00

38 KiB
Raw Blame History

План Source RS: одиночная игра, совместимая с GMod

Статус: план согласован, начало исполнения (2026-10-12). Каталог: ~/garrysmod-rs. Связано: PLAN.md (общий план), PROBLEMS.md (боли), RECON.md (реверс).

Каждый этап — рабочий коммит и проверяемый результат, а не заготовка.


Главный принцип: совместимость, а не тождество внутренностей

Решение пользователя: «1 в 1 не надо прям, но надо совместимость».

Это меняет критерий готовности. Нас интересует не внутреннее устройство GMod, а то, что снаружи него видно и от чего зависит чужой код:

Что должно работать Почему это «совместимость»
Контент карт и аддонов: BSP, VTF, VMT, MDL, VTX, а также lua/ с аддона аддон физически не запустится без этого
Lua-API: Entity(), ents.*, Vector/Angle/Color, хуки, таймеры, file, surface, sound, render, cam, Curves код аддона должен выполняться без правок
Поведение игрока и физика геймплей аддона завязан на это
Формат карт: сущности читаем так же, как в Source иначе нельзя открыть чужую карту
Сетевой протокол (уже сделано) мультиплеер

Что не требуется: внутренние структуры движка, точное совпадение побайтовых раскладок в нестандартных случаях, поддержка всего, что есть в base_ents GMod.

Практическое следствие: там, где можно, сверяемся с исходниками Source.NET и реальными файлами, а не угадываем; а публичные Lua-имена и сигнатуры берём из настоящих lua/includes/ GMod, а не придумываем.


Порядок (подтверждён)

A → B → C → D → E, причём свет (C) раньше game.dll (E2) — так решил пользователь.


Часть A. Найденные дефекты (то, что сломано прямо сейчас)

Часть пунктов пользователь заметил визуально, часть подтверждена замером по реальному gm_construct.bsp.

A1. Коллизия: скорость не гасится о поверхность

Симптом (пользователь): «подхожу в стену, отпускаю клавиши — входит внутрь чуть-чуть, потом выталкивает».

Причина (crates/engine/src/player.rs:432): move_aabb() возвращает только (Aabb, on_ground) и не трогает self.velocity. Каждый кадр игрок сдвигается на velocity * dt внутрь стены, получает выталкивание и оказывается на грани — визуально «ныряет» и отскакивает. Горизонтальная скорость в стену никогда не обнуляется.

Как в Source: CMoveData отдаёт clipNormal, скорость проектируется на плоскость контакта: v -= n * min(0, dot(v, n)).

Исправление: move_aabb возвращает нормали контакта; Player::update гасит компоненту скорости внутрь каждой нормали и обнуляет velocity.z при on_ground. Плюс явное торможение при отпускании клавиш.

Приёмка: тест «в стену на полной скорости → после отпускания стоит неподвижно, |velocity.xz| < 1, положение не дрейфует»; тест на отсутствие просадки в стену при dt = 1/60 и скорости 250.

A2. Коллизия: твёрдость браша определяется неверно

Симптомы: «невозможно пройти в здания, хотя проход есть», «вода твёрдая».

Причина (player.rs:43): const TRIGGER: i32 = 0x4000_0000; и проверка if contents == TRIGGER { continue }. То есть твёрдым считается всё, кроме ровно 0x40000000 — включая браши с contents == 0 (по спецификации Source CONTENTS_SOLID = 0x1, всё остальное не твёрдо) и браши воды/тумана.

Замер по gm_construct.bsp (2203 браша, лумп 18):

contents смысл брашей сейчас твёрдые по спецификации
0x08000001 SOLID 1130 да да
0x00000001 SOLID 996 да да
0x00000000 EMPTY (не твёрдый) 70 да ✗ нет
0x08000080 не SOLID 3 да ✗ нет
0x18000008 не SOLID 2 да ✗ нет
0x10000020 WATER 1 да ✗ нет
0x30000008 не SOLID 1 да ✗ нет

Итого 77 из 2203 брашей (3.5%) считаются твёрдыми ошибочно — невидимые стены в проходах и твёрдая вода озера.

Исправление: твёрдым считать браш, у которого установлен бит CONTENTS_SOLID (0x1). Отдельно разобрать воду (0x20), туман (0x40), окна (0x2) и решётки (0x8): они не останавливают игрока, но влияют на жидкость и свет.

Приёмка: счётчик брашей по contents в логе; на gm_construct ожидается 2126 твёрдых и 77 не твёрдых; тесты «проход в здание открыт», «в воду можно войти».

A3. Коллизия: нет broadphase для брашей

2203 браша проверяются целиком на каждом подшаге, в каждой из 4 итераций разрешения. Для displacement'ов есть сетка, для брашей — нет.

Исправление: 2D-сетка по XY с AABB каждого браша; запрос кандидатов по расширенному AABB хода.

Приёмка: замер времени кадра коллизии до/после; цель — стабильные 60 fps при обычном движении.

A4. Рендер: отсечения граней нет вообще

Симптом: «видно обратную сторону брашей».

Причина (crates/render/src/world_renderer.rs): cull_mode: None во всех шести пайплайнах (строки 247, 292, 385, 483, 613).

Уточнение пользователя: в оригинале сквозь стены тоже видно — но там это намеренно (вид сквозь стену на HUD), а не отсутствие backface culling.

Исправление: CullMode::Back для мира и моделей; для двусторонней геометрии — отдельный проход CullMode::Front с инверсией нормали (как в Source: сначала фронт с инверсией, затем бэк).

Приёмка: в галерее gm_construct не видно изнанки стен.

A5. Рендер: деталь-текстуры не поддержаны → нет песка у озера

Симптом: «у озера трава должна переходить в песок, песка нет».

Причина: в crates/formats/src/bsp.rs перечислены 64 лумпа, и деталь-текстур среди них нет. В Source 1 они лежат в GAME_LUMP как под-лумп detailtextures.dll (LUMP_DETAILTEXTURES = 12) и накладываются поверх базовой текстуры. Шейдер world.wgsl (38 строк) читает только base_texture и умножает на вершинный light.

Исправление:

  1. Разбор GAME_LUMP (запись лумпа 16 байт: fileofs, disksize, offset, fourCC), под-лумп 12 → список (текстура, scaleX, scaleY).
  2. Проекция деталь-текстуры на мировые координаты по UV грани.
  3. Шейдер: дополнительные сэмплы со смешением, $detail из VMT.

Приёмка: у озера gm_construct виден переход травы в песок.

A6. Белая нетекстурированная поверхность (было: «белые панели на 0 0 0»)

Симптом по скриншоту: большая белая polygonal-поверхность, лежащая на земле рядом с озером (камера на pos -1070 1565 533). Контур с прямыми отрезками и острыми углами, поверхность явно повторяет рельеф местности. Окружающие дома, трава, дороги и вода (слева, тёмно-синяя) выглядят нормально.

Это не пропы. Проп — это объёмная модель с чёткими гранями; здесь же плоская поверхность, граница которой идёт по рельефу. Значит это браш- или displacement-геометрия, у которой не разрешилась текстура — материал рисуется fallback-белым (Material::fallback, world.rs:739).

Ранее я проверил пропы и ничего подозрительного не нашёл: пропов без origin нет, с origin = "0 0 0" нет, наш код порождает всего 9 моделей (5 × prop_dynamic lights_flescent01a, 4 × prop_physics мебели) — все с корректными координатами. Это гипотеза отвергнута, версия снимка была неверной.

Кандидаты причины, по убыванию вероятности:

  1. Не найдена VTF-текстура для части поверхностей — fallback рисует белым. Особенно вероятно для displacement-поверхностей у воды: у них отдельные texinfo, которые не всегда есть в списке.
  2. Не разрешён VMT — тогда материал падает в тот же белый fallback.
  3. Отдельная поверхность, которой полагается деталь-текстура, но базовая не загрузилась (связано с A5).

Действие (одна итерация, даёт точный ответ): дамп всех материалов мира — { имя VMT, имя VTF, загрузилась ли, причина отказа } плюс AABB каждого батча. По скриншоту видно, что проблема локальная (один район карты), значит список «отвалившихся» текстур будет небольшим и читаемым.

Побочный вывод: белый fallback — плохая диагностика и плохое поведение. Нужна проверка в doctor (см. B4) и осмысленная заглушка: тёмно-шахматная вместо белой, чтобы выделять проблему, а не маскировать её.

A8. Знаки и порядок осей при повороте моделей

rotate_model_vector строит базис forward/left/up, тогда как Source использует forward/right/up. Модельная система координат MDL — правая (X вперёд, Y влево, Z вверх), поэтому знак оси Y критичен: ошибка разворачивает модель зеркально и ломает нормали.

Это не «на глаз» — сверяем с эталонным AngleVectors и закрываем unit-тестом на известных углах (0 0 0, 0 90 0, 0 0 90, 90 0 0), фиксируя ожидаемые векторы из формулы Source.

A7. Вода выглядит неправильно

Симптом: «просто какое-то отражение неба, никакой текстуры, твёрдая».

Причины:

  • отражение — зашитая константа vec3<f32>(0.64, 0.76, 0.90) в water.wgsl:49, а не окружение;
  • одна поверхность с discard по знаку facing, тогда как в Source их две ($abovewater 1 и 0) — верх и изнанка;
  • нет преломления, поглощения по глубине, блика солнца, нормалей из текстуры, $fogcolor-затухания;
  • твёрдость — из A2.

Исправление: две поверхности; отражение из окружения; преломление через цвет буфера; поглощение по расстоянию; specular от солнца; плюс A2.


Часть B. Фундамент: система энтити

Без неё не появляется ни свет, ни Lua над энтити, ни физика. Свет — это тоже энтити (light_environment, light, light_spot).

B1. Ядро (crates/engine/src/entity.rs)

  • EntityIndex(u16) — слот + generation для безопасных «умерших» указателей;
  • Entity — classname, keyvalues, transform (origin/angles/local-origin), флаги (solid, movetype, model, render/rendergroup);
  • EntityWorld — пул, spawn(kv), remove, итерация по classname, отложенное удаление (как в Source, чтобы не инвалидировать ссылки в кадре);
  • лимиты из файлов: максимум энтити, максимум ключей, максимум длины значения (правило #1 AGENTS.md — никаких unwrap на данных из файлов).

B2. Спавн из карты: build_world перестаёт вручную порождать модели — он скармливает лумп ENTITIES в EntityWorld, а рендер и коллизия читают из неё. Это убирает белые панели как класс проблем и даёт всем остальным этапам единый источник истины.

B3. Коллизия по энтити: браши func_*/trigger_*/info_null с корректной твёрдостью; AABB/OBB для моделей; trigger-зоны отдельным слоем.

B4. Отладка: ent_list / ent_info <classname> — иначе каждый следующий дефект снова придётся ловить на глаз.

Приёмка: ent_list показывает все энтити gm_construct с корректными координатами; коллизия func_* работает.


Часть C. Освещение (включая солнце)

Сейчас в шейдере один попиксельно интерполированный light: vec3 из вершин — меньше, чем в Source даже на простых картах.

C1. Свет из карты: light, light_spot, light_dynamic (в gm_construct: 108 light + 28 light_spot), light_environment — это и есть солнце (направление, цвет, интенсивность, флаг «небо»).

C2. Модель освещения в шейдере: динамический свет (солнце + до N источников в радиусе) поверх статического; источники ограничить по экрану и расстоянию.

C3. Sun-тени: каскадные shadow maps (в Source 3–4 каскада). Начать с одного каскада 2048².

C4. Lightmap: лумп LIGHTING/LIGHTING_HDR (12 МБ в gm_construct) — попиксельно вместо вершинного; даёт качественный свет в помещениях.

C5. Материал: Phong-подобная модель — diffuse + specular + fresnel; $phong/$specular/$color2 из VMT; самозатенение от карты нормалей.

C6. Небо и env_*: env_sky, env_fog, env_sun, env_tonemap, env_cubemap, env_skybox.

Приёмка: на gm_construct видны солнечные тени от построек, помещения освещены не плоско, вода отражает окружение, есть спекуляр на металле.


Часть D. Lua поверх энтити

Текущий lua (1278 строк) — print, hook.*, timer.Simple, concommand.*, заглушки. Ни типов, ни энтити.

D1. Типы: Vector, Angle, Color как userdata с метаметодами GMod (арифметика, Angle:Forward(), Vector:Distance, :Angle).

D2. Entity(index) и методы: GetPos/SetPos, GetAngles/SetAngles, GetClass/GetModel/SetModel, Remove, IsValid, PhysicsInit*.

D3. ents.*: Create, FindByClass, FindInSphere, GetAll, GetCount.

D4. Сетевые поля: SetNW*/GetNW* (в одиночке локальные, но с тем же API, чтобы потом не переписывать аддоны).

D5. Библиотеки GMod: file.* (с песочницей по AGENTS.md), surface.*, sound.*, render.*, cam.*, Curves, system.*, scrconcmd, LocalPlayer. http — только после песочницы.

Приёмка: скрипт из гмода выполняется; список функций совместимости (набранный из настоящих lua/includes/modules/) проходит.


Часть E. Одиночная игра как в GMod

E1. Player: ходьба по ступеням (step-up ~18 units), лестницы, приседание с плавным переходом, плавание (Start Swim), ноклип, падение с уроном.

E2. game.dll на Lua: загрузка gamemodes/sandbox/gamemode/init.lua — списки ents, weapons, NPCs, хуки Initialize, Think, PlayerSpawn. Это то, что делает GMod «GMod’ом», а не просто картой Source.

E3. Оружие и предметы: weapon_*, item_* через Lua.

E4. Физика пропов: Jolt — prop_physics уже приходят с карты, но не падают и не сталкиваются.

E5. NPC: начиная с базовых npc_*.


Порядок и разумная дистанция до GMod

GMod — это ~1 млн строк C++ плюс 15 лет аддон-экосистемы. Полное «1 в 1» недостижимо как финальное состояние, но достижимо как наращиваемое покрытие: A → B → C → D → E даёт к каждому моменту работающий одиночный клиент.

Этап Что даёт пользователю
A «стены не пускают, вода не твёрдая, белая поверхность устранена, песок появился»
B карта описывается объектами — база для всего
C солнце, тени, нормальный свет
D Lua-аддоны запускаются на нашей карте
E одиночная игра, совместимая с аддонами

Честное ограничение: «все энтити» — сотни классов. Но base_ents GMod — это Lua-скрипты, а не C++. Когда появится исполнение lua/entities/ и lua/weapons/, значительная часть этих классов заработает сама, из исходников GMod. Поэтому наш код на этих этапах делает только то, чего в Lua нельзя: низкий уровень графики, физика, загрузка ресурсов.

Свой набор «тяжёлых» классов держим минимальным: prop_physics, func_door, func_button, trigger_*, light_*, env_*, info_*. Остальное — из Lua.


Решения, принятые пользователем

  1. Скриншот получен → A6 переформулирован: это не панели пропов, а нетекстурированная браш/displacement-поверхность. Версия про «панели в 0 0 0» отвергнута.
  2. AngleVectors → вопрос снят, работа делается как A8: сверка с эталонной формулой Source и фиксация unit-тестом, без опоры на память.
  3. Приоритет → свет (C) раньше game.dll (E2).

Что осталось уточнить

Ничего блокирующего. Дополнительно уточню по ходу:

  • белый fallback заменить на тёмно-шахматный (A6) — согласую при реализации, это меняет вид карты при недогруженных текстурах.

Часть C. Прогресс

C0. Источники света из карты (сделано)

engine::world::collect_lights() разбирает light_environment (солнце), light, light_dynamic, light_fluoro_dynamic, light_spot. На gm_construct находится 65 источников, из них 1 солнце и 22 прожектора.

Шейдер world.wgsl получил uniform-блок источников: солнце (до 4), точечные и прожекторные (до 64, отбираются ближайшие к камере), плюс ambient.

Три ловушки, на которые напоролся (все проверены на gm_construct):

  1. Четвёртая компонента _light означает разные вещи. У light и light_spot это РАДИУС освещения (_light 255 255 255 300), а у light_environment — множитель яркости солнца (_light 242 234 218 560). Если считать это яркостью везде, лампы дают вклад ×300 и карта выбивается в белый.

  2. Яркость нельзя домножать в цвет. Если умножить цвет на scale при разборе и учесть scale ещё раз в шейдере, получается квадрат яркости. Цвет нормируется к единичному среднему, яркость хранится отдельным полем scale.

  3. Радиус 0 означал «светить во everywhere». Функция затухания при радиусе 0 возвращала 1.0 на любом расстоянии, и 22 прожектора складывались в сумму. Теперь у ламп без _radius подставляется 300.

  4. Лайтмапа уже содержит запечённое солнце. Это ключевой момент: in.light — это и есть запечённый свет компилятора. Динамическое солнце сверху нужно только для направленной подсветки и будущих теней, поэтому его вклад намеренно мал (SUN_BRIGHTNESS_SCALE = 900 даёт ~0.62 от карты). Наивная попытка добавить солнце «как есть» давала пересвет бетона в чистый белый.

Итоговые нормировки подобраны рендерами на gm_construct и лежат константами SUN_BRIGHTNESS_SCALE, POINT_BRIGHTNESS_SCALE, AMBIENT рядом с кодом.

Осталось по части C

  • Попиксельная лайтмапа (LUMP_LIGHTING) вместо вершинного света.
  • Тени от солнца (каскадные shadow maps).
  • Phong-модель: specular + fresnel, $phong/$specular из VMT.
  • Ambient из _ambient карты вместо константы.
  • Полный набор env_*.

Часть B2. Физика: Rapier3D (сделано)

B2. Решение: мир не переезжает в Rapier

Rapier обслуживает только динамические тела. Геометрия карты остаётся брашевой и разбирается нашим кодом (player.rs) — он проверен и быстрее на тысячах брашей. Статика в Rapier добавляется явно и только из данных.

B2.1. Четыре дефекта, найденных измерением

Все четыре нашлись не «правдоподобным декодированием», а замером на карте. Порядок важен: каждый следующий дефект был бы невидим, пока не закрыт предыдущий.

  1. Рельеф раздувался в 15 277 коллайдеров. Сетка по LAND_CELL = 64 дублировала каждый перекрытый треугольник в каждую ячейку: 89 треугольников давали 15 277 коллайдеров. Заменено одним объединённым trimesh с дедупликацией вершин по кванту 1/16 юнита: 11 776 треугольников → 6 148 вершин, 1 коллайдер. Точнее (никакого огрубления на швах ячеек) и на порядки дешевле.

  2. Уровень gm_construct построен брашами, а не displacement'ами. Рельеф покрывает лишь часть карты; игрок ходит по 2 126 брашам. Без брашей все пропы падали насквозь. PhysicsWorld::add_brushes восстанавливает вершины браша перебором троек плоскостей (Крамер): точка, подходящая под все плоскости — вершина выпуклой оболочки. У осевого браша 6 плоскостей = 20 троек. Замер: 2121 построен, 5 пропущено (вырожденные).

  3. Broad-phase Rapier наполняется только на step(). После вставки геометрии луч её не видел: «под пропом пола нет» при 2 121 браше в мире. Лечится явной регистрацией broad_phase.set_aabb() при вставке статики.

  4. setup_physics стирал построенное. Начинался с self.physics = PhysicsWorld::new() и обнулял браши, добавленные строкой выше. Мир физики больше не пересоздаётся.

B2.2. Тест поймал баг, который замер бы не увидел

add_brushes создавал коллайдеры, но не увеличивал счётчик: лог показывал static = 1, хотя брашей было 2 121. Тест box_brush_lands_where_the_box_is упал на static_bodies() == 1 и указал прямо на строку.

B2.3. Подтверждение на gm_construct

Две независимые системы коллизий сверены друг с другом: брашевая (Collision) и Rapier. Обе находят пол под пропами на z = −96 (Rapier −96.0, брашевая −95.9). После 400 кадров все 4 prop_physics осели: speed = 0.0, sleeping = true.

B2.4. Ограничения

  • Коллайдер пропа — AABB модели, а не vcollide. Source берёт физическую модель из MDL; мы её не разбираем. AABB всегда не меньше модели и всегда из данных, но для наклонённых пропов он «жирнее» настоящего.
  • Только prop_physics*. У prop_static физики в GMod нет.
  • FPS просел: 10–21 в headless-прогоне. Требуется замер отдельно с физикой и без — 2 121 выпуклый коллайдер может быть причиной.

B2.5. Что осталось по части B2

  • vcollide из MDL вместо AABB.
  • Брашевые func_door / func_button как ограничивающие тела.
  • env_explosion через apply_explosion (метод готов, вызов из Lua — нет).
  • Замер FPS с физикой и без, чтобы понять цену 2 121 коллайдера.

Часть D1. Lua-типы GMod (сделано)

D1. Что сделано

crates/lua/src/gmod.rs — Vector, Angle, Color как userdata с арифметикой, полями на запись и tostring в формате GMod. Установлены в глобалы вместе с math.Clamp.

Решения, важные для совместимости:

  • Пропущенных аргументов не бывает. Vector() и Vector(1, 2) обязаны работать: так вызывается бытовой код Sandbox. Отсутствующее значение заменяется нулём, а не ошибкой.
  • Деление на ноль даёт 1, а не inf. inf в позиции пропа отравляет геометрию всего объекта и потом всплывает в отрисовке.
  • NaN из скрипта не доезжает до физики. Color(0/0, ...) даёт белый, Vector(0/0, 1, 2) — вектор без NaN.
  • Компоненты цвета обрезаются в 0..255, а не отвергаются: Color(1.5 * 300, 0, 0) — обычное дело в скриптах.
  • Source — левая система координат: Angle(0,0,0):Forward() смотрит вдоль +X, Angle(0,90,0):Forward() — вдоль +Y, знак Z у forward отрицательный.

D1.1. Дефект чтения чисел в mlua 0.11

Value::as_number() в mlua 0.11 различает только Value::Number и молча возвращает None на Value::Integer. LuaJIT отдаёт целые именно как Integer, поэтому наивное чтение теряло все целочисленные аргументы:

Vector(1, 2, 3)  -->  Vector(0, 0, 0)

Выглядело это как «арифметика не работает», хотя дело было в чтении чисел. Собственный value_number() принимает Number, Integer и строку (числом). Тест на это обязателен: без него Vector() с целыми просто тихо отдаёт нули.

D1.2. Шум тригонометрии

cos(90°) в f32 — это 6.1e-17, а не ноль. Без обнуления Angle(0, 90, 0):Forward() печатает -0.00000004371139 1 -0, и такие значения копятся в преобразованиях координат. Порог — 1e-6.

D1.3. Особенности API mlua 0.11, на которые ушло время

  • Для userdata нет автоматического FromLua: чтение только через AnyUserData::borrow::<T>(). Lua::from_value отсутствует, есть FromLua::from_lua(value, lua).
  • Поля — add_fields (трейт UserDataFields), методы и метаметоды — add_methods (трейт UserDataMethods). Один трейт на всё не работает.
  • add_meta_method получает (&Lua, &T, A): значение объекта идёт вторым аргументом, лишние аргументы Lua (__unm(a, a)) отбрасываются через ().
  • add_function получает объект как AnyUserData первым элементом кортежа.

D1.4. Что осталось по части D1

  • Entity() и объект сущности: GetPos/SetPos/GetClass/IsValid.
  • ents.*: Create, GetAll, FindInSphere, GetPlayer.
  • Связка с движком: glua не должен зависеть от engine, обмен — через колбэки, которые регистрирует клиент.
  • Vector:Angle(), Vector:Distance между сущностями.

Часть D2. Мост Lua к движку (сделано)

D2.1. Устройство

glua не зависит от engine. Движок отдаёт поведение через трейт glua::ents::Host, реализация которого живёт в клиенте (crates/client/src/host.rs). Иначе получился бы цикл зависимостей и Lua-слой нельзя было бы тестировать в одиночку.

Мир переведён в Rc<RefCell<World>>: скрипт обращается к нему из хуков в любой момент, пока кадр держит заимствование. Все обращения идут через try_borrow, потому что borrow при пересечении даёт panic от RefCell, а падать из-за безобидного скрипта клиент не должен.

D2.2. Что доступно скрипту

Entity(n), ents.GetAll/GetByClass/FindInSphere/Create, player, а также GetPos/SetPos/GetAngles/SetAngles/GetClass/GetModel/ GetVelocity/IsValid/Remove/GetName.

Entity(0) — игрок, Entity(9999) — невалидная сущность, а не ошибка: скрипты routinely проверяют результат через IsValid().

D2.3. Найденный рассинхрон (важный)

move_prop обновлял позу экземпляра пропа, но не запись в пуле сущностей. Проверено измерением: ents.GetByClass('prop_physics'):GetPos() давал z = -64, пока физика уже опустила проп на z = -84. «Данные» и «картинка» разошлись, и заметить это можно было только глазами.

Теперь поза пишется в оба места всегда, даже если геометрию пересчитать нечем (нет модели): проп без модели всё равно стоит правильно, он просто не рисуется. Возвращаемое значение означает именно «геометрия пересчитана».

Проверка после правки: Lua читает z = -84.15848, физика даёт -84.15847778320313 — совпадение до последнего бита.

D2.4. Границы

  • SetModel, SetColor, PhysicsInit принимаются и ничего не делают: принимать вызов нужно, чтобы скрипты не падали, но обещать нечего.
  • GetVelocity делит скорость тела на шаг 1/60, а не читает «скорость за последний кадр»: иначе скрипт получил бы юниты/кадр вместо юнитов/с.
  • Созданная из Lua сущность не получает модель и тело физики.
  • Игрок — None, пока клиент его не зарегистрировал.

D2.5. Что осталось по части D2

  • Vector:Distance, Vector:Angle, GetModel-обратная привязка.
  • Entity:GetTable/SetField и сетевые функции.
  • Регистрация сущности игрока, чтобы Entity(0) и player совпали.
  • ents.Create должен уметь заводить тело физики для prop_physics.
  • Подключение файлов gamemodes/sandbox как есть.