Батч 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 чист.
Батч 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 чист.
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).
Зафиксирован план достижения одиночной игры, совместимой с GMod.
Главный принцип — совместимость, а не тождество внутренностей: контент
карт и аддонов, Lua-API, поведение игрока, формат сущностей.
Часть A — семь найденных дефектов, в том числе подтверждённых замером
по реальному gm_construct.bsp: 77 из 2203 брашей ошибочно считаются
твёрдыми, скорость не гасится о контакт, нет отсечения граней,
нет деталь-текстур.
Наш первый netchan-пакет был на 22 байта короче эталонного — ровно на
cl_playermodel\0male07\0. Без него сервер не знает, какую модель ставить
нашему энтити-игроку.
Набор конваров вынесен в client::userinfo_pairs() и теперь сверяется с
эталоном настоящего GMod (новая фикстура fixtures/classicbox_client_hello.bin,
511 байт из пакета #24) попарно, включая порядок. При том же имени наш пакет
теперь совпадает с эталонным побайтово: тело идентично в 513 из 514 байт,
различаются только checksum/challenge из-за другой сессии и один остаточный
бит добивки.
Раскладка взята из Source.NET (NetMessages.cs), т.к. эталонного pcap с этими
сообщениями нет — в захвате потеряно 2576 серверных пакетов именно на участке,
где сервер шлет таблицы (docs/RECON.md 13.13).
Важно: у SVC_SendTable нет имени таблицы, в отличие от раскладки Source 2013
SDK, на которую я ориентировался раньше.
Попутно найден баг через круговые тесты: префикс ':' у строковой таблицы
нужно смотреть (PeekUBitLong), а не читать — иначе теряется первый символ
имени ('userinfo' читался как 'serinfo') и дальше по потоку едет смещение.
Число классов ограничено MAX_SERVER_CLASSES=4096: значение приходит из сети.
Source.NET в ReadSubChannelData при фрагменте вне диапазона или нулевой
длине делает data.Return() и выходит — это штатная ситуация для
опоздавшего/дублирующего фрагмента. Наш код возвращал NetError, из-за чего
терялись все остальные сообщения пакета, а с ними и signon-блок.
Взят прямо из Source.NET (IConnectionlessPacketHandler.cs, enum PacketFlag):
Reliable=1, Compressed=2, Encrypted=4, Split=8, Choked=16, Challenge=32.
Из-за неверного бита в пакетах сервера с выставленным битом 3 читался
лишний байт, reliable_state и challenge уезжали на 8 бит.
Добавлены тесты на константы флагов и на заголовок с Choked.
Раскладка 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.
Layout одиночного блока, подтверждённый перебором по pcap настоящего GMod:
4 (подканал) + 2 (потоки) + 1 (single-block) + VarInt32 = 23 бита.
Мы писали 24 бита (лишний бит «сжатие»), из-за чего сервер читал длину
с неверного смещения и отбраковывал фрагмент — reliable-поток не читался,
m_bSendServerInfo не выставлялся.
Также набор USERINFO-конваров приведён к реальному (28 шт. с net_maxroutable).
Захват настоящего GMod (ClassicBox, 8091 пакетов) показал: реальный клиент
шлёт в первом netchan-пакете rel=0x0000, а мы ставили 1. Это поле означает
«какие подканалы мы хотим получить», в начале оно нулевое.
Проверка по InReliableState показала: исходные 4 бита и LSB-first верны
(and $0xF / позиция +4 в ProcessPacket; rol в bf_write::WriteByte).
Правки 12f94ae и 50bb363 были регрессией и откачены.
Настоящая причина отсутствия 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.
Главная причина отсутствия 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).
Проверка поля клиента (0x5af) в GMod-сервере: для удалённого клиента
сигнон-блок не отправляется, пока флаг (вероятно m_bFullyAuthenticated,
ставится при успешной асинхронной валидации тикета) не выставлен.
Симптомы совпадают: connect принимается, netchan работает, сообщения
доходят, но ServerInfo не приходит. Дальнейшие шаги — в docs/RECON.md §13.2.
- Регулируемые экспериментальные ручки: 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
принимается) — следующая задача; все зацепки в доке.
- Современный 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.
- Вода: детект 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: форматы листьев/граней, лайтмапы, скайбокс, вода.