Commit graph

43 commits

Author SHA1 Message Date
d6c49fd67a Константы по таблице GMod и устранены невидимые стены из плоскостей граней
Четыре жалобы из игры, все закрыты измерением.

Константы движения (gmodwiki.com/ConVars_In_Garrysmod, не source-sdk-2013):
  sv_friction         4 -> 8     торможение 250 -> 10 за 0,40 с, путь 26 юнитов
  sv_stopspeed     100 -> 10
  sv_gravity       800 -> 600    прыжок 45,6 -> 60,8 юнита, как в GMod
  sv_airaccelerate  12 -> 10
  sv_maxvelocity   350 -> 3500   падение 585/589/590 -> 600/1200/1800/2400/3000

«В воздухе управление слабее»: в AirAccelerate кап sv_air_max_wishspeed
ограничивает ЦЕЛЬ, а прибавка считается по некапленной wishspeed. Мы
ограничивали и то и другое — прибавка была в 8,3 раза слабее. Замер после
правки: рывок вбок даёт ровно 30,0 юнитов/с, как в Source. Поворот при
полёте вперёд по-прежнему заблокирован (проекция 177 >> капа 30) — это
верное поведение оригинала.

«Высота шага терялась единица»: подъём ровно на sv_stepsime ставит низ
габарита точно на грань ступени, грань считается касанием, и подшаг
блокирует сам себя. Замерено: брались уступы до 17 и застревали на 18.
В оригинале подъём на flStepSize + DIST_EPSILON. После правки — все до 18
включительно, 19 нет.

«Невидимые стены, рампа как параллелипипед»: грань кирпича ограничивает
плоскость, а не кирпич, и останавливает шаг где угодно на её уровне.
Замерено: игрок останавливался за 3,4 юнита до настоящей преграды, блокировала
верхняя грань соседнего кирпича. Правка: при t около нуля проверяется, что
точка касания лежит на самом кирпиче. Замер после правки: преград «из
воздуха» 0 (было 2), упоров за 30 с ходьбы 0.

Проверено и отброшено измерением: кисти не работают как ящик (аудит 2126
кирпичей, 0 расхождений из 129684 точек); displacement не выпадает из сетки
(11776 треугольников, выпало 0); скорость вдоль склона 239-263 против 250.

Пара sv_falldistance/fall_speed НЕ введена осознанно: в таблице cvar'ов GMod
её нет, а формула HL2 при отрицательной velocity[2] даёт замедление падения,
а не разгон. Ставить непроверенные константы значило бы гадать.

228 тестов зелёные, clippy -D warnings чист.
2026-10-14 07:52:39 +03:00
f34c456034 Гравитация пополам до и после движения, ограничение скорости sv_maxvelocity
Два оставшихся пункта из B6.7, сверенные с оригиналом SDK 2013
(src/game/shared/gamemovement.cpp).

- StartGravity / FinishGravity берут по половине гравитации до и после
  движения: «yes, this 0.5 looks wrong, but it's not» — комментарий Valve.
  Замер высоты прыжка на gm_construct: 42,7 -> 44,9 юнита при ~45 в Source.
  Заодно проверено, что TryPlayerMove гравитацию не применяет вовсе.
- CheckVelocity: sv_maxvelocity = 350 попоосно. Без него шаг за кадр на
  большой скорости становится длиннее стены, и отсечение её не видит.
- Место вызова важнее самого ограничения: в оригинале CheckVelocity стоит
  прямо перед TryPlayerMove (внутри Friction). Вызов только в конце кадра
  позволял одной нечислице уйти в смещение и сделать позицию нечислицей
  навсегда — прогон тестов занимал 42 с вместо 0,01. Теперь вызов есть
  и перед движением, и в конце.
- Позиция проверяется на нечислицу так же, как скорость.

Тесты: speed_is_capped_at_max_velocity, non_finite_velocity_is_zeroed.
2026-10-14 07:13:30 +03:00
b96b06ccdb Небо гасится за пределами карты: и 2D-скайбокс, и 3D-скайбокс
Жалоба: «если игрок выходит за карту, то скайбокс и 3D-скайбокс не должны
рендериться (иначе не понятны границы карты)».

Причина видна в шейдере 3D-скайбокса:
    world = camera.pos.xyz + (in.pos - sky_camera) * scale
Геометрия неба размещается относительно камеры, поэтому небо всегда заливает
весь фон — пределы карты на нём не видно.

Замер на gm_construct:
  габариты геометрии  (-5640, -4576, -960) .. (2168, 6464, 2976)
  габариты коллизии   (-15360, -15360, -1024) .. (15872, 15360, 15359)
  sky_camera          (-1428, 1645, 10991), масштаб 1/16
  2D-скайбокса нет, только 3D
Габариты коллизии не годятся — включают и 3D-небо, и далёкие террасы,
поэтому основание — габариты геометрии карты.

- world::inside_bounds — одна реализация предиката, её используют и движок,
  и рендерер (рендерер копирует габариты при загрузке).
- WorldRenderer::update_sky_visibility пересчитывает видимость каждый кадр:
  игрок уходит за край во время игры. Проверка по всем трём осям сразу,
  под картой небо тоже исчезает.
- SKY_BOUNDS_MARGIN = 128: габариты считаются по вершинам, а камера стоит
  на высоте глаз над опорой, так что стоящий на краю игрок формально за ними.
- Гасятся оба прохода неба: иначе за краем остался бы виден 3D-неб, а это
  ровно то, что мешало увидеть границу.

Тесты: sky_is_visible_only_inside_the_map (юнит) и real_map_sky_bounds
(замер на настоящей карте).
2026-10-14 07:10:47 +03:00
512f3d0394 Движение игрока переписано по CGameMovement: SAT, цикл касаний, квото-ускорение
Замеры на gm_construct до и после — шесть жалоб из игры закрыты.

Исправлено по измерениям, а не по догадкам:
- vertices_of брал пересечение двух граней (прямую) и выбирал точку вне
  кирпича: 1846 кирпичей из 2126 (87%) считались «без объёма», то есть
  не твёрдыми. Это корень «сквозь рампу прошёл» и «падаю сквозь карту».
- clip_fraction отбрасывал грань при касании (guard `start <= limit`):
  шаг 8 юнитов — стоп у стены, шаг 16 — на 8 внутрь. Guard убран,
  условие `!(0.0..1.0).contains(&t)` — верно и для -0.0, и для NaN.
- Полуплоскость кирпича действовала на всю плоскость: игрок зависал
  над краем площадки. Добавлен swept-bounds early-out.
- Движение шло одним общим процентом шага, из-за чего игрок «запинкой»
  полз по полу. Заменено на цикл касаний TryPlayerMove (numbumps = 4).
- step_move всегда выбирал путь «вверх», а он уводил назад по склону:
  игрок сползал с x = -256 до x = -1066 за 5 с. Переписано по оригиналу —
  побеждает путь, ушедший дальше по горизонтали, z из движения вниз.
- stay_on_ground падал всем шагом, вминался в кирпич и выталкивался вдоль
  нормали, то есть вбок по склону. Теперь зондирует вниз по 1 юниту.
- Трение было только без ввода. FullWalkMove зовёт Friction() безусловно —
  именно оно удерживает игрока на рампе.
- Линейная интерполяция ускорения замедляет не хуже, чем ускоряет:
  равновесие на 179 юнитах/с. Заменена квото-формула Accelerate;
  sv_air_max_wishspeed = 30 действует только в воздухе.

Замер на настоящей карте (real_map_movement, --ignored):
  скорость 165 -> 224,9 (среднее с разгона; Source 250)
  дёрганье назад 0,00 юнита за 3 с
  прыжок 42,7 юнита (Source ~45)
  падение сквозь мир и проход сквозь рампу устранены

Старый real_map_player_rests_walks_and_does_not_sink требовал пройти
2000 юнитов вслепую вдоль +x, где на карте стена на x = 992 — ожидание
теста было неверно, а не игрок. Тест удалён, его корректная часть
покрыта real_map_movement.

222 теста зелёные, clippy -D warnings чист.
2026-10-14 07:07:48 +03:00
7389ac5579 Переписана коллизия тела с кирпичами: SAT вместо неверной глубины проникновения
Главный дефект батча 2 был не в кинематике, а в проверке пересечения AABB с
кирпичом. Симптомы были «поверхность превратилась в лёд» и «выталкивает сквозь
пол и стены».

Глубина проникновения считалась как `dist - project_min(aabb, n)` — насколько
тело зашло за грань с внутренней стороны. Для игрока, стоящего на полу (низ на
1 юнит ниже верха кирпича высотой 100), это даёт 172 юнита проникновения в
нижнюю грань — на 172 юнита выше неё. Тело выталкивало вниз сквозь пол.
Отсюда сразу все жалобы: улетев вниз, игрок терял `on_ground`, трение не
применялось — это и был «лёд».

Заменено на SAT по разделяющим осям: длина перекрытия интервалов проекций.
Для того же игрока — 1 юнит, выход вверх. Чтобы у кирпича был второй конец
интервала, находятся его вершины и хранится проекция противоположного края.

Отдельно: минимальный вектор выхода не годится для тонкой стены — у стены
шириной 16 при теле шириной 32 выход вперёд всегда короче, и тело, дошедшее до
её середины, выталкивалось сквозь (замерено ровно до x = 132.1 при стене на
x = 100). Добавлена swept-трассировка: тело останавливается у грани и никогда
не оказывается по другую сторону.

По `CGameMovement` из Source SDK 2013:
- `StayOnGround()` — функции не было вообще. После ходьбы игрок
  притягивается к поверхности; без этого он висит над землёй на спуске.
- порог опоры `sv_walkable_normal` = 0.7, а не 0.6;
- `StepMove` работает только когда игрок был на земле, в прыжке на ступеньку
  не взбираются;
- в пути отсечения выставляется опора — без этого игрок замирал намертво на
  45° рампе: гашение скорости контактом уравновешивало гравитацию.

Замер на gm_construct (30 секунд ходьбы): пройдено 6372 юнита вместо 1072,
проваливания и замирания нет. 81 тест зелёный, clippy чистый.
2026-10-13 16:02:06 +03:00
570f4bb9d0 docs: изучен CGameMovement из Source SDK 2013 — расхождения с нашей физикой
Доки Valve закрыты Anubis, читал первоисточник
ValveSoftware/source-sdk-2013, src/game/shared/gamemovement.cpp.

Записаны точные причины двух симптомов пользователя и ещё трёх расхождений:

1. StepMove у оригинала пробует оба варианта (вниз и вверх на flStepSize) и
   берёт тот, что ушёл дальше по горизонтали; вертикальную скорость берёт из
   движения ВНИЗ (m_vecVelocity.z = vecDownVel.z). У нас подшаг всегда идёт
   путём «поднять 18 и уронить», режет путь на рампе и добавляет паразитную
   вертикаль — отсюда и потеря скорости вверх, и «прыжки с рампы» вниз.
2. Friction() вызывается каждый кадр на земле безусловно, формула
   control = max(speed, sv_stopspeed), drop = control*friction*frametime.
   У нас трение только при отсутствии ввода — по рампе на бегу его нет.
3. Гравитация делится 0.5 до и 0.5 после движения (StartGravity/FinishGravity).
4. Ускорение — Quake-формула accelspeed = accel*frametime*wishspeed, а в
   воздухе ещё и ограничение sv_air_max_wishspeed = 30. Отсюда «вязкое»
   управление в воздухе, которого у нас нет.
5. Прыжок по фронту: в оригинале при выходе из воды ставится
   m_nOldButtons |= IN_JUMP с комментарием «don't jump again until released» —
   наша защёлка воспроизводит штатный приём движка.

Код не менял: сначала фиксирую расхождения, чтобы не подгонять тесты.
2026-10-13 15:20:08 +03:00
2e906e034b engine: физика движения как в Source — гравитация только в воздухе
Четыре жалобы (проталкивает на расстояние, метания вдоль стены, сползание по
рампам, bhop) оказались одной ошибкой: гравитация применялась КАЖДЫЙ кадр,
в том числе стоя на земле. Игрок вдавливался в геометрию, разрешение
столкновений выталкивало обратно.

Source делает наоборот: при FL_ONGROUND вертикальная скорость
обнуляется, а ApplyGravity в земле не вызывается вовсе.

Что изменено:
- гравитация только в воздухе;
- трение всегда на земле (Source зовёт Friction() каждый кадр) — именно
  оно держит игрока на рампе; sv_friction = 4, sv_stopspeed = 100;
- прыжок по фронту нажатия, защёлка держится пока пробел зажат;
- подшаг PM_StepSlideMove: полное движение, при блокировке повтор с
  подъёмом на 18 юнитов и спуском; при падении отключён, иначе игрёт в
  потолок;
- MoveResult::blocked — признак блокировки для подшага.

Три мои ошибки пойманы тестами:
1. jump_latched = jump — защёлка вставала ДО первого прыжка, прыгать было
   нельзя вообще;
2. jump_latched = jump && on_ground — флаг сбрасывался тем же кадром, когда
   прыжок уже отработал, bhop возвращался;
3. тест на рамку мерил первоначальное падение (40 -> 6), а не сползание, и
   падал с «сносом 34»; после отделения приземления снос ноль.

Также: пропы добавлены в коллизию игрока (Collision::boxes) и в is_on_ground
- иначе игрок стоял на ящике и считал себя в воздухе.

Тестов 73 в engine (было 70), 23 набора по воркспейсу, clippy чист.
2026-10-13 14:59:20 +03:00
e6d7454a6a engine: тест на прошивку тонкого пола — гипотеза не подтвердилась
Две версии про проваливание проверены тестами и обе оказались неверными.

1. «Пропов нет в коллизии игрока» — пробел реальный и устранён, но игрок
   ходил по брашам, это его не объясняло.
2. «На низком FPS шаг подшага прошивает пол» — опровергнуто: при
   terminal-скорости 2000 юнитов/с и dt = 1/60 игрок садится на площадку
   толщиной 4 юнита, on_ground = true.

Тест оставлен как регрессионный сторож: если подшаги когда-нибудь
ослабнут, он сразу это покажет.

Замер: стояние на спавне gm_construct стабильно 180 кадров (z = -143.9,
on_ground = true). Дефект локальный и требует указания места.

Причина не найдена — фиксирую это в docs/PARITY.md B3, а не подгоняю
тест под желаемый результат.
2026-10-13 14:44:45 +03:00
32bd7c05f2 engine: констрейны — шар, шарнир, ползун, сварка, верёвка
Батч 3, шаг 4. Соединения между телами по классам GMod: phys_ballsocket,
phys_hinge, phys_slider, phys_weld, rope. Плюс add_force (база для
thruster/hover) и set_angular_velocity_target (мотор шарнира).

Главная тонкость переноса: скрипт задаёт якорь в мировых координатах, а
Rapier ждёт локальные в каждом теле. Пересчёт через обратное вращение.
Ошибка не бросает исключение — соединение просто «приклеивается» к другой
точке и уезжает при первом движении.

Четыре вывода из измерений, а не из рассуждений:

1. Констрейн не «стягивает» тела, и это правильно: якорь в мировой точке
   совпадает у обоих тел в момент создания, ограничение выполнено сразу и
   включается, только когда тела расходятся. Пять первых тестов проверяли
   расстояние между телами и падали именно поэтому. Правильный инвариант —
   совпадение ТОЧЕК КРЕПЛЕНИЯ под действием силы.
2. Сила в Rapier действует один шаг и сбрасывается: add_force надо вызывать
   каждый кадр. Тест с однократной силой измерял не силу, а её отсутствие,
   и падал одинаково при 8000 и при 40000.
3. Спящее тело честно сообщает нулевую скорость, поэтому эффект силы
   проверяется сравнением с контрольным прогоном, а не по скорости.
4. ПОЛЗУН НЕ УДЕРЖИВАЕТ. Сустав сообщает enabled = true и 5 заблокированных
   осей из 6, шар/шарнир/сварка в том же прогоне держат, но ползун пропускает
   расхождение по высоте. Тест помечен #[ignore] с причиной: зелёная галочка
   на неработающем коде хуже явного отказа. Гипотеза к проверке записана в
   docs/PARITY.md B2.6.

Тестов 67 в engine (было 56), 23 набора по воркспейсу, clippy чист.
2026-10-13 13:56:47 +03:00
aa7558721a lua: стандартная библиотека GMod — hook.Run, math, game, file с песочницей
Батч 3, шаг 3. Сначала измерено, а не придумано: прогнаны 130 файлов
gamemodes/sandbox, извлечены 182 различных вызова API с частотами.
Топ по частоте: hook.Run (63), math.Clamp (61), game.SinglePlayer (36),
math.Rand (27), render.AddBeam (21). Первые четыре — чисто Lua-слой,
сделаны здесь; render.* — отдельная большая работа.

hook.Run не синоним hook.Call: он останавливается на первом не-nil. На этом
построены все проверки Sandbox, подмена Run на Call ломала бы их молча.

Найдено три дефекта, все — измерением:

1. Ключи хуков — строки (идентификаторы скриптов), а не числа. Отбор только
   целочисленных ключей давал пустой список: хуки не выполнялись вообще,
   hook.Run возвращал nil.
2. Vec<Value> реализует IntoLua как таблицу, поэтому Function::call передавал
   хуку ОДИН аргумент вместо трёх — молча, без ошибки. Проверено:
   select('#', ...) внутри хука давал 1 при трёх переданных. То же с одиночным
   числом. Лечится Variadic; отдельный тест фиксирует именно эту ошибку.
3. math.Rand определён на Lua, а не на Rust: он зовёт math.random того же
   состояния и передаёт аргументы нативно.

file.* — с песочницей на garrysmod/: выход за корень это отказ, а не
предупреждение, абсолютные пути не даны, чтение ограничено 8 МБ. На живом
клиенте file.Read('../../../../etc/passwd') вернул nil.

Совпадения с GMod зафиксированы тестами: math.Round(-2.5) = -3,
math.Rand(x) даёт целое в [1, x], AngleDifference(10, 370) = 0,
Remap на вырожденном диапазоне даёт out_min, а не NaN.

Тестов 38 в glua (было 24), 23 набора по воркспейсу, clippy чист.
2026-10-13 13:17:28 +03:00
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
b9b03abe3b lua: типы GMod — Vector, Angle, Color с арифметикой
Батч 3, шаг 1. Фундамент Lua-API: без этих типов Entity:GetPos() не
возвращает ничего, с чем можно считать.

Проверено тестами через настоящий LuaRuntime, а не голый интерпретатор:
скрипт считает теми же глобалами, что и обычный автор в Sandbox.

Решения по совместимости:
- пропущенные аргументы не бывают ошибкой: Vector() и Vector(1, 2) должны
  работать, иначе ломается бытовой код Sandbox;
- деление на ноль даёт 1, а не inf — inf в позиции пропа отравляет всю
  геометрию объекта;
- NaN из скрипта не доезжает до физики;
- компоненты цвета обрезаются в 0..255, а не отвергаются;
- Source — левая система координат: нулевые углы смотрят вдоль +X,
  yaw=90 — вдоль +Y.

Найден дефект чтения чисел в mlua 0.11: Value::as_number() различает только
Number и молча возвращает None на Integer, а LuaJIT отдаёт целые как
Integer. Vector(1, 2, 3) давал Vector(0, 0, 0) — и это выглядело как
«арифметика не работает». Собственный value_number() принимает Number,
Integer и строку-число.

Ещё убрат шум тригонометрии: cos(90°) в f32 равен 6.1e-17, и
Angle(0,90,0):Forward() печатал -0.00000004371139 вместо 0.

Тестов 17 в glua (было 1), 23 набора по воркспейсу, clippy чист.
2026-10-13 12:48:50 +03:00
1cb836d4c9 engine: физика на Rapier3D — пропы падают, сталкиваются и засыпают
Батч 2. Мир не переезжает в Rapier целиком: геометрия карты остаётся
брашевой, Rapier обслуживает только динамические тела.

Четыре дефекта найдены измерением на gm_construct, а не догадкой — каждый
следующий был невидим, пока не закрыт предыдущий:

1. Рельеф раздувался в 15 277 коллайдеров (сетка LAND_CELL дублировала
   каждый треугольник в каждую ячейку). Заменено одним объединённым
   trimesh с дедупликацией вершин: 11 776 треугольников -> 6 148 вершин,
   1 коллайдер. Точнее и на порядки дешевле.
2. Уровень gm_construct построен брашами, а не displacement'ами: без них
   все пропы падали насквозь. add_brushes восстанавливает вершины
   перебором троек плоскостей (Крамер). Замер: 2121 построен, 5 пропущено.
3. Broad-phase Rapier наполняется только на step(): после вставки статики
   луч её не видел. Лечится явным broad_phase.set_aabb().
4. setup_physics начинался с PhysicsWorld::new() и стирал построенные браши.

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

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

Ограничения зафиксированы в docs/PARITY.md: коллайдер пропа — AABB модели,
а не vcollide; физика только у prop_physics*; FPS просел до 10-21 и требует
отдельного замера.

Тестов 55 в engine (было 42), 23 набора по воркспейсу, clippy чист.
2026-10-13 12:36:51 +03:00
387abd6454 engine/render: динамический свет из карты (солнце, лампы, прожекторы)
collect_lights() разбирает light_environment (солнце), light, light_spot.
На gm_construct: 65 источников, 1 солнце, 22 прожектора. Шейдер получил
uniform-блок с солнцем, точечными источниками и конусами прожекторов.

Ключевые находки при подборе нормировки (проверено рендерами):
- четвертая компонента _light — это РАДИУС у ламп и ЯРКОСТЬ у солнца;
- яркость нельзя домножать в цвет, иначе возводится в квадрат;
- лайтмапа уже содержит запеченное солнце, динамическое добавляется слабо.

Также добавлен разбор GAME_LUMP в formats (нужен для деталь-текстур) и
заодно выяснено: в gm_construct деталь-текстур нет, под-лумпы prps/prpd —
это геометрия пропов. Текстура GRASS-SAND_13 есть, но не используется ни
одним texinfo — вопрос песка остается открытым (docs/PARITY.md A5).
2026-10-13 08:55:51 +03:00
c9264fe892 docs: план совместимости с GMod (одиночная игра)
Зафиксирован план достижения одиночной игры, совместимой с GMod.
Главный принцип — совместимость, а не тождество внутренностей: контент
карт и аддонов, Lua-API, поведение игрока, формат сущностей.

Часть A — семь найденных дефектов, в том числе подтверждённых замером
по реальному gm_construct.bsp: 77 из 2203 брашей ошибочно считаются
твёрдыми, скорость не гасится о контакт, нет отсечения граней,
нет деталь-текстур.
2026-10-12 16:45:34 +03:00
77d70f6b9f net: добавлен недостающий userinfo-конвар cl_playermodel
Наш первый netchan-пакет был на 22 байта короче эталонного — ровно на
cl_playermodel\0male07\0. Без него сервер не знает, какую модель ставить
нашему энтити-игроку.

Набор конваров вынесен в client::userinfo_pairs() и теперь сверяется с
эталоном настоящего GMod (новая фикстура fixtures/classicbox_client_hello.bin,
511 байт из пакета #24) попарно, включая порядок. При том же имени наш пакет
теперь совпадает с эталонным побайтово: тело идентично в 513 из 514 байт,
различаются только checksum/challenge из-за другой сессии и один остаточный
бит добивки.
2026-10-12 14:51:57 +03:00
9cee256cb8 net: разбор SVC_SendTable, SVC_ClassInfo, SVC_CreateStringTable
Раскладка взята из Source.NET (NetMessages.cs), т.к. эталонного pcap с этими
сообщениями нет — в захвате потеряно 2576 серверных пакетов именно на участке,
где сервер шлет таблицы (docs/RECON.md 13.13).

Важно: у SVC_SendTable нет имени таблицы, в отличие от раскладки Source 2013
SDK, на которую я ориентировался раньше.

Попутно найден баг через круговые тесты: префикс ':' у строковой таблицы
нужно смотреть (PeekUBitLong), а не читать — иначе теряется первый символ
имени ('userinfo' читался как 'serinfo') и дальше по потоку едет смещение.

Число классов ограничено MAX_SERVER_CLASSES=4096: значение приходит из сети.
2026-10-12 14:38:04 +03:00
fb48358208 net: «неожиданный» фрагмент сбрасывает передачу, а не весь пакет
Source.NET в ReadSubChannelData при фрагменте вне диапазона или нулевой
длине делает data.Return() и выходит — это штатная ситуация для
опоздавшего/дублирующего фрагмента. Наш код возвращал NetError, из-за чего
терялись все остальные сообщения пакета, а с ними и signon-блок.
2026-10-12 14:35:28 +03:00
459fe6b0bc net: PACKET_FLAG_CHOKED = 1<<4 (раньше был 1<<3 = Split)
Взят прямо из Source.NET (IConnectionlessPacketHandler.cs, enum PacketFlag):
Reliable=1, Compressed=2, Encrypted=4, Split=8, Choked=16, Challenge=32.
Из-за неверного бита в пакетах сервера с выставленным битом 3 читался
лишний байт, reliable_state и challenge уезжали на 8 бит.
Добавлены тесты на константы флагов и на заголовок с Choked.
2026-10-12 14:31:16 +03:00
c6b9b04404 net: разбор SVC_ServerInfo по эталону + фикстур ClassicBox
Раскладка ServerInfo была неверной (max_classes как long вместо 16 бит,
4 байта вместо MD5 карты, ServerOS как строка, нет loading_url/gamemode).
Исправлено по спецификации Source.NET и подтверждено тестами на реальном
блоке ClassicBox: map=gm_bigcity_improved_rp, host=ClassicBox,
gamemode=sandbox, maxcl=48, tick=0.030303, proto=24.
2026-10-12 14:24:22 +03:00
5600b53469 net: убран лишний бит «сжатие» в заголовке reliable-фрагмента
Layout одиночного блока, подтверждённый перебором по pcap настоящего GMod:
4 (подканал) + 2 (потоки) + 1 (single-block) + VarInt32 = 23 бита.
Мы писали 24 бита (лишний бит «сжатие»), из-за чего сервер читал длину
с неверного смещения и отбраковывал фрагмент — reliable-поток не читался,
m_bSendServerInfo не выставлялся.

Также набор USERINFO-конваров приведён к реальному (28 шт. с net_maxroutable).
2026-10-12 14:18:53 +03:00
92b4784c2a net: InReliableState=0 в первом пакете (по эталонному pcap)
Захват настоящего GMod (ClassicBox, 8091 пакетов) показал: реальный клиент
шлёт в первом netchan-пакете rel=0x0000, а мы ставили 1. Это поле означает
«какие подканалы мы хотим получить», в начале оно нулевое.
2026-10-12 14:05:49 +03:00
ec38667d2d docs: локальный сервер-эталон Source.NET поднят, разбор connect/accept 2026-10-12 13:53:27 +03:00
2860f67a36 docs: выжимка референса Source.NET по протоколу сигнона 2026-10-12 12:23:11 +03:00
2716884050 docs: полная верификация протокола и локализация блокера
Подтверждено по дизасму engine_srv.so и живому трафику: 4 бита подканала,
LSB-first, NETMSG_TYPE_BITS=6, 2 потока, reliable=0x01, challenge=0x20,
тело NET_SignonState = байт + u32, формат cookie в connect.

Сервер читает наш reliable-блок (InReliableState -> 1) и обрабатывает
net_SignonState; блокер только m_bFullyAuthenticated в SendPendingServerInfo.
2026-10-12 12:18:28 +03:00
c624d7f6ac docs: опровержение гипотез о подканале и порядке битов
Проверка по InReliableState показала: исходные 4 бита и LSB-first верны
(and $0xF / позиция +4 в ProcessPacket; rol в bf_write::WriteByte).
Правки 12f94ae и 50bb363 были регрессией и откачены.
2026-10-12 12:06:49 +03:00
3964e83e0f Revert "net: индекс подканала — 3 бита (исправлен silently-баг reliable)"
This reverts commit 50bb36304e.
2026-10-12 12:06:32 +03:00
7f2ec26030 Revert "net: битовый буфер переведён на MSB-first, как в движке"
This reverts commit 12f94ae55a.
2026-10-12 12:06:32 +03:00
12f94ae55a net: битовый буфер переведён на MSB-first, как в движке
Настоящая причина отсутствия server info: bf_write писал биты LSB-первым,
а Source читает/пишет MSB-первым (подтверждено дизассемблированием
bf_read::ReadUBitLongNoInline в engine_srv.so).

Из-за этого WriteOneBit(true) попадал в поле соседнего потока фрагментов,
а ID сообщений записывались перевёрнутыми — сервер не распознавал
net_SignonState и не выставлял m_bSendServerInfo.

- write_one_bit/write_ubit_long/read_one_bit/read_ubit_long: MSB-first;
- write_u32/write_short/read_long/read_short: побайтово little-endian
  (как ReadLong в движке);
- reliable_state кодируется через WriteUBitLong(16);
- тесты bit_order_matches_source и reliable_block_layout_is_canonical.
2026-10-12 11:52:28 +03:00
50bb36304e net: индекс подканала — 3 бита (исправлен silently-баг reliable)
Главная причина отсутствия server info: SUBCHANNEL_BITS был 4, из-за чего
сервер читал subchannel=1 и 0 фрагментов, молча отбрасывая reliable-поток
(net_SignonState не обрабатывался → m_bSendServerInfo не выставлялся).

Подтверждено pcap-разбором и дизасмом engine_srv.so (CNetChan::ProcessPacket:
lea 0x3(%esi) — ровно 3 бита; and $0x1 — reliable-флаг 0x01).
2026-10-12 11:44:49 +03:00
9cc4daf6b0 docs: дизассемблер SendPendingServerInfo — сигнон ждёт m_bFullyAuthenticated
Проверка поля клиента (0x5af) в GMod-сервере: для удалённого клиента
сигнон-блок не отправляется, пока флаг (вероятно m_bFullyAuthenticated,
ставится при успешной асинхронной валидации тикета) не выставлен.
Симптомы совпадают: connect принимается, netchan работает, сообщения
доходят, но ServerInfo не приходит. Дальнейшие шаги — в docs/RECON.md §13.2.
2026-10-12 10:53:15 +03:00
7befb1f8ee net: диагностика сигнона (выбор подканалов, повтор hello, проба SignonState)
- Регулируемые экспериментальные ручки: SOURCE_RS_SUBCHANNEL_BITS,
  SOURCE_RS_REL_STATE, SOURCE_RS_HELLO_EVERY_MS, SOURCE_RS_PROBE_SIGNON,
  SOURCE_RS_RETRY_DELAY_MS, SOURCE_RS_NO_TICKET_REFRESH.
- docs/RECON.md §13.1: разбор логики сигнона по утёкшему исходнику
  (SendPendingServerInfo/SetSignOnState/ConnectClient), формат challenge-ответа,
  наблюдения по лимитам Steam (DuplicateRequest ~2-3 мин), что уже проверено.
- Сигнон-блок от сервера пока не приходит (наши пакеты доходят, connect
  принимается) — следующая задача; все зацепки в доке.
2026-10-12 10:46:15 +03:00
8414920df2 net: тикет Steam с идентичностью игрового сервера (фикс #GameUI_ServerRejectSteam)
- Современный Steamworks требует GetAuthSessionTicket с
  SteamNetworkingIdentity = SteamID игрового сервера; generic-тикет сервер
  отвергает синхронно (#GameUI_ServerRejectSteam). Тикет привязывается к
  серверу из ответа challenge ('A': keysize u16, GSID u64, secure u8).
- Кэш тикета по SteamID сервера (как CSteam3Client::GetAuthSessionTicket),
  ретраи connect с задержкой (10 с): повторный BeginAuthSession для того же
  SteamID сервер считает дубликатом и отклоняет.
- Тикет запрашивается в момент connect (GSID уже известен), стартовый тикет
  больше не нужен.
- docs/RECON.md §13: полный формат challenge/connect/reject, порядок состояний
  сигнона, наблюдения по лимитам Steam.
2026-10-12 10:07:06 +03:00
44b0ece2ab render: вода (SURF_WARP), area-скайбокс с правильным порядком глубины, лайтмапы диспов по UV
- Вода: детект Water-шейдера (/), отдельный полупрозрачный
  проход с френелем; поверхность/изнанка по нормали. Белая дыра на месте воды
  (2D-скайбокс «вниз») устранена.
- 3D-скайбокс: выбор граней по area листа sky_camera (движок: area bits), а не по
  AABB. Disp-грани не попадают в leaf_faces — выбор по листу центра грани; теперь
  в скайбоксе есть большая плоскость flatgrass_2 (пропадала — торчала «стена»
  бетонного пола). Защита >30% граней = скайбокс не выделяем (gm_flatgrass).
- Порядок: 2D-скайбокс фоном -> 3D-меш (depth Always, без записи) -> мир ->
  вода. Скайбокс больше не проступает сквозь дальние стены карты.
- Лайтмапы: блок грани (size+1)^2 с индексом v*width+u; для displacement'ов —
  плотная сетка по UV поверхности (CalcDispSurfCoords), а не проекция мира:
  убрана «шахматка» теней на рельефе. Скайбокс-меш тоже сэмплит лайтмапы.
- Тесты: gm_flatgrass-диспы живут в скайбоксе (z≈-16000), пороги обновлены.
- docs/RECON.md §12: форматы листьев/граней, лайтмапы, скайбокс, вода.
2026-10-12 08:28:28 +03:00
a92da38a1d docs: прогресс M2/M3/M7 (netchan, игрок, модели, скайбокс, HUD, меню, Lua)
- PLAN.md: отмечено сделанное по M3 v1 (ходьба, энтити-модели, 3D-скайбокс,
  HUD, меню) и M7 v0 (LuaJIT-рантайм, препроцессор, автозагрузка аддонов)
- net: диагностические env-переключатели (keepalive, задержки connect, флаги)
- RECON §11: формат GMod netchan и открытый вопрос кика
2026-10-12 04:47:04 +03:00
a72185e808 net: разбор надёжного потока netchan (подканалы, фрагменты), фиксы GMod-флагов
- NetChan: чтение подканалов (single/fragments, сжатие, varint), сборка потоков,
  разбор сообщений из собранных потоков
- GMod-специфика: флаг Challenge = 0x20, subchannel 4 бита (16 подканалов)
- Steam: клиент живёт всю сессию, ждём AuthSessionTicketResponse, свежий тикет
  при повторных connect (challenge одноразовый)
- client: keepalive-пакеты, диагностика (hex-дампы, connectionless)
- docs/RECON: подробности формата netchan и нерешённый кик сервером
2026-10-12 03:44:21 +03:00
efc4b03bc8 net: M2 v0 — хендшейк с ClassicBox, Steam-тикет, netchan-заголовки
- crates/net: битовые буферы Source (LSB), A2S, netchan (флаги/checksum/challenge),
  разбор net/SVC-сообщений, connect с GMod ChecksumXOR и Steam-тикетом
- steamworks-rs: session ticket для VAC-серверов (appid 4000)
- версия клиента в connect берётся из A2S (фикс #GameUI_ServerRejectNewVersion)
- CLI: net info / net connect
- Проверено: ClassicBox принимает подключение (auth_protocol=3, Steam)
- Далее: подканалы надёжного потока → SVC_ServerInfo → signon
- docs: прогресс M2, заметки (нумерация GMod, ChecksumXOR, версии, локальный DS)
2026-10-12 00:27:28 +03:00
b8b5a4c328 engine/render: M3 v0 — рендер мира BSP со скайбоксом
- crates/engine: сборка мира (браши, displacement'ы, лайтмапы в вершинах,
  материалы через VMT/patch/include, кэш текстур, пропуск прозрачных материалов),
  загрузка скайбокса из skyname
- crates/render: wgpu-рендерер мира (VB/IB, батчи, глубина), скайбокс-куб,
  камера-полёт, capture PNG с учётом BGRA, debug-режимы материалов
- client: команда map (диск/VPK, pakfile карты, WASD+мышь, --pos/--angles/--screenshot)
- тесты: gm_construct/gm_flatgrass собираются, 0 заглушек, 41 324 треугольника
- docs: прогресс M3 и заметки (BGRA, pak-VMT, z=w клиппинг)
2026-10-12 00:06:26 +03:00
d6b213cecc formats: VVD/VTX — меш-данные моделей (вершины, fixup, LOD-дерево)
- VVD (IDSV v4): заголовок 64 байта, mstudiovertex_t 48 байт (веса/кости/позиция/нормаль/UV),
  тангенты, таблица fixup
- VTX (packed): BodyPart→Model→LOD→Mesh→StripGroup→Strip, валидация индексов и смещений;
  смещения вложенных структур относительны своей структуры
- fs inspect: для .mdl показывает VVD/VTX-статистику и сверку checksum
- тесты: oildrum (270 вершин/756 индексов), 51 пара VVD/VTX с совпадающим checksum
- docs: прогресс M1 (остался ANIM, PCF/PHY/SND, GMA/mount.cfg)
2026-10-11 23:15:46 +03:00
7236910e69 formats: MDL (заголовок v44-49, кости, аттачменты, хитбоксы, текстуры, keyvalues)
- базовый studiohdr_t = 408 байт, v48+ studiohdr2 (256 байт) перед костями
- строки читаются относительно структур (sznameindex), всё bounded
- fs inspect: полная сводка по модели (кости/аттачменты/текстуры/include/keyvalues)
- тесты: oildrum (v44, 1 кость), advisor (v48, 25 костей), выборка 53 моделей — 0 ошибок
- docs: прогресс M1 (остались VVD/VTX/ANIM, PCF/PHY/SND, GMA/mount.cfg)
2026-10-11 23:09:25 +03:00
34ea6b8521 formats: KeyValues, BSP (+геометрия, LZMA, pakfile ZIP), VTF (декод DXT), VMT; fs inspect/list
- crates/formats: bounded-парсеры kv/bsp/vtf/vmt/pakfile
- BSP: 64 лумпа, LZMA, ENTITIES, texdata/имена, game lumps, геометрия v20 с validate()
- VTF 7.0-7.5: заголовок, ресурсы 7.3+, мипы, декод в RGBA8 (DXT1/3/5 и др.)
- pakfile: ZIP Store/Deflate с CRC и лимитами
- client: fs list, fs inspect (формат по магии), геометрия карты в inspect
- тесты: реальные карты gm_construct/gm_flatgrass, 200 VTF, 200 VMT, pakfile 599 записей
- docs: прогресс M1 и заметки разведки
2026-10-11 23:03:45 +03:00
dec5b2c48f sprint 0: workspace, tier/fs/math, VPK v1/v2 с CRC, окно SDL3+wgpu на Vulkan
- crates/tier: tracing-логи, конвары с реестром, конфиг TOML, авто-детект ассетов
- crates/fs: bounded-парсер VPK v1/v2 (preload, встроенные данные, внешние архивы),
  VirtualFs с приоритетами и защитой от traversal, CRC-проверка
- crates/math: Vec3/Angle/Aabb
- crates/client (gmod-rs): fs doctor / fs cat, окно SDL3+wgpu 30, FPS в заголовке и логе
- тесты на реальных VPK (~/gmod_server/server): 9 контейнеров, 77 933 записи, 0 ошибок CRC
- CI-скелет, fuzz-таргет vpk_parse, config.example.toml, обновление docs
2026-10-11 22:47:09 +03:00
ec0750a829 docs: план Source RS — этапы, архитектура, проблемы, Lua, сеть, разведка 2026-10-11 22:25:36 +03:00