Батч 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 чист.
38 KiB
План 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.
Исправление:
- Разбор
GAME_LUMP(запись лумпа 16 байт:fileofs, disksize, offset, fourCC), под-лумп 12 → список(текстура, scaleX, scaleY). - Проекция деталь-текстуры на мировые координаты по UV грани.
- Шейдер: дополнительные сэмплы со смешением,
$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 мебели) — все с
корректными координатами. Это гипотеза отвергнута, версия снимка была неверной.
Кандидаты причины, по убыванию вероятности:
- Не найдена VTF-текстура для части поверхностей —
fallbackрисует белым. Особенно вероятно для displacement-поверхностей у воды: у них отдельныеtexinfo, которые не всегда есть в списке. - Не разрешён VMT — тогда материал падает в тот же белый fallback.
- Отдельная поверхность, которой полагается деталь-текстура, но базовая не загрузилась (связано с 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 их две ($abovewater1 и 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.
Решения, принятые пользователем
- Скриншот получен → A6 переформулирован: это не панели пропов, а нетекстурированная браш/displacement-поверхность. Версия про «панели в 0 0 0» отвергнута.
AngleVectors→ вопрос снят, работа делается как A8: сверка с эталонной формулой Source и фиксация unit-тестом, без опоры на память.- Приоритет → свет (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):
-
Четвёртая компонента
_lightозначает разные вещи. Уlightиlight_spotэто РАДИУС освещения (_light 255 255 255 300), а уlight_environment— множитель яркости солнца (_light 242 234 218 560). Если считать это яркостью везде, лампы дают вклад ×300 и карта выбивается в белый. -
Яркость нельзя домножать в цвет. Если умножить цвет на
scaleпри разборе и учестьscaleещё раз в шейдере, получается квадрат яркости. Цвет нормируется к единичному среднему, яркость хранится отдельным полемscale. -
Радиус 0 означал «светить во everywhere». Функция затухания при радиусе 0 возвращала 1.0 на любом расстоянии, и 22 прожектора складывались в сумму. Теперь у ламп без
_radiusподставляется 300. -
Лайтмапа уже содержит запечённое солнце. Это ключевой момент:
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. Четыре дефекта, найденных измерением
Все четыре нашлись не «правдоподобным декодированием», а замером на карте. Порядок важен: каждый следующий дефект был бы невидим, пока не закрыт предыдущий.
-
Рельеф раздувался в 15 277 коллайдеров. Сетка по
LAND_CELL = 64дублировала каждый перекрытый треугольник в каждую ячейку: 89 треугольников давали 15 277 коллайдеров. Заменено одним объединённым trimesh с дедупликацией вершин по кванту 1/16 юнита: 11 776 треугольников → 6 148 вершин, 1 коллайдер. Точнее (никакого огрубления на швах ячеек) и на порядки дешевле. -
Уровень gm_construct построен брашами, а не displacement'ами. Рельеф покрывает лишь часть карты; игрок ходит по 2 126 брашам. Без брашей все пропы падали насквозь.
PhysicsWorld::add_brushesвосстанавливает вершины браша перебором троек плоскостей (Крамер): точка, подходящая под все плоскости — вершина выпуклой оболочки. У осевого браша 6 плоскостей = 20 троек. Замер: 2121 построен, 5 пропущено (вырожденные). -
Broad-phase Rapier наполняется только на
step(). После вставки геометрии луч её не видел: «под пропом пола нет» при 2 121 браше в мире. Лечится явной регистрациейbroad_phase.set_aabb()при вставке статики. -
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как есть.