Commit Graph

5 Commits

Author SHA1 Message Date
sasheg dev 5b451c38da Мир перестаёт стучить при ходьбе
Статтеры были не на сервере, а в клиенте, и стоили дорого.

Измерено: перевод чанка в игровой формат шел через setBlockState, а он
каждый раз обновляет карту высот и карту света. Чанк стоил 10-156 мс на
главном потоке, их было 208, и переход через границу чанка упирался в
325 мс за один тик.

Сделано:

- Секции чанка пишутся прямо в формате игры. Палитра и упакованные
  индексы строятся игровым же классом, так что формат совпадает по
  определению, а заливка по блокам и карты высот и света не нужны.
  Чанк подешевел с 25 мс до 1-2 мс, это в 15-25 раз.
- Обработка входящих пакетов ограничена тремя миллисекундами на тик.
  Колонка чанков, пришедшая за один раз, теперь размазывается по тикам:
  не влезшее ждет следующего, это 0.2 секунды, и глазом не видно.
- Сгенерированный чанк больше не считается грязным. Террейн есть чистая
  функция от зерна и координат, он и так восстановится за пару
  миллисекунд, а писать его стоило 64 килобайта на чанк. Ходьба по миру
  больше не гоняет двадцать мегабайт по диску. На диск пишется только то,
  во что игрок построился.

Проверено: 169 чанков при заходе, медленных (больше 4 мс) три, остальные
быстрее; на диске ноль файлов, потому что игрок ничего не строил.
2026-10-19 20:36:42 +03:00
sasheg dev 46784aa88f Клиент говорит с сервером по нашему протоколу
Раньше клиент умел только показывать меню. Теперь он заходит на сервер,
получает наш мир и рисует его.

Как это устроено:

- Наш сетевой слой (net/rustnotmc/client/net) читает наш протокол и
  переводит его пакеты в пакеты игры. Игра остаётся игрой: у неё есть
  мир, игрок, сущности и рендер, и все они работают. Не переписывать их
  ради того, чтобы нарисовать тот же мир иначе, незачем.
- Пакеты игры, которые не имеют соответствия у нас, не отправляются, а
  наоборот: перемещение игрока и ломание блока переводятся в наши.
- Экран входа игры (GuiConnecting) заменён наш: он спрашивает у лаунчера,
  кем пришёл игрок, и подставляет это в рукопожатие.
- Соединение качается из тика игры, а не из экрана. Рукопожатие, которое
  зависит от того, что экран ещё на экране, ломается alt-tab'ом.

Вход теперь через лаунчер, а не внутри игры:

- Лаунчер уже умеет логинить игрока и отдаёт игре имя, uuid и токен
  (--username, --uuid, --accessToken). Это та же роль, что у Yggdrasil в
  ванилле, только лаунчер наш.
- Сервер спрашивает лаунчер, один из ли наш этот игрок, и заодно получает
  бан-лист. Токен используется один раз и не хранится.
- Свои аккаунты сервера, когда настроен лаунчер, закрыты: сервер с двумя
  дверями не имеет дверей. Открыть их можно флагом, но намеренно.
- Если игру запустили без лаунчера, экран всё равно спрашивает имя и
  пароль: это путь для своего сервера и для разработки.

Проверено живьём: клиент вошёл по токену лаунчера, сервер спросил
лаунчер, лаунчер сказал "свой", игрок получил мир, 210 чанков пришли и
лежат на диске, рельеф нарисован.

Что чинилось по дороге:

- Minecraft.setIngameFocus() возвращал на главное меню, когда окно
  получало фокус. Игрок как раз кликает в окно, чтобы напечатать пароль, и
  экран входа исчезал в этот самый момент.
- Ответ лаунчера разбирается, а не ищется подстрокой: {"valid": true} и
  {"valid":true} это один и тот же ответ, и сервер, который понимает
  только второй, ломает работающий лаунчер.
2026-10-19 20:10:19 +03:00
sasheg dev 68dfea27b5 Мир на сервере: чанки, генерация, сохранение и поток игрока
Сервер умел принимать игроков и не умел ничего, что можно увидеть. Мир
был не написан: ни чанков, ни генерации, ни сохранения, ни игровых
пакетов.

Сделано:

- Пакет world: ChunkPos, Chunk, Blocks (теперь в протоколе), WorldGenerator,
  ChunkStorage, World. Генерация чистая функция от зерна и координат, без
  java.util.Random и без общего состояния, потому что чанк может
  появиться через месяц после соседнего, на другой машине, и обязан
  совпасть с ним побайтово. Сохранение — файл на чанк, запись во временный
  файл и перенос, версия и контрольная сумма: половина записанного файла
  это мир, который молча испортился.
- Снимок чанка и запись блока теперь под одним замком. Раньше копия могла
  сняться посреди записи блока, и на диск уходила половина изменения.
- Player: позиция, вид, список отправленных чанков, проверка досягаемости
  блока. Клиент не может переписать блок, до которого не дотянулся.
- Сервер держит мир, крутит его по тику, выкидывает давно не тронутые
  чанки, шлет чанки вокруг игрока и рассылает изменения блоков и передвижения
  тем, кто их видит.
- Клиентский сетевой слой в игре еще не тронут: это следующий шаг.

Проверено: 45 тестов сервера. Из них 25 на мир (детерминизм, круг
сохранения, автосохранение, вытеснение, отказ битому файлу) и 9 сквозных
через настоящий сокет: игрок входит, получает 25 чанков, чанк совпадает с
террейном сервера, блок ломается и остаётся сломанным после перезапуска
сервера, два игрока видят друг друга.
2026-10-19 18:58:07 +03:00
sasheg dev 620f136361 Серверная база со своим логином
Теперь можно зарегистрировать аккаунт, поднять сервер и зайти в него.

Пароль не ходит по сети и не хранится на сервере в обратимом виде. Клиент
называет аккаунт, сервер отдаёт его соль и одноразовый вызов для этого
соединения, клиент тянет из пароля ключ через PBKDF2-HMAC-SHA256 и
подписывает им вызов вместе с именем. Пароль пересекает сокет ноль раз, а
запись обрезать нельзя: сервер не знает исходного пароля.

Отсюда важное следствие, которое сразу отозвало одну ошибку в протоколе:
соль обязана прийти раньше, чем клиент что-то доказывает, поэтому аккаунт
называется в Hello, а не в Auth. Иначе клиент получает соль не того
аккаунта и не может вывести ключ.

Учётная запись хранится как PBKDF2 соль, счётчик раундов и хеш. Раунды
лежат рядом с записью, а не в исходниках: число, существующее только в
коде, однажды становится причиной того, что аккаунт невозможно проверить.

Имена уникальны независимо от регистра: Player и player — это один
аккаунт. Иначе игрок получает двух персонажей и не понимает, какой его.

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

Токены живут только в памяти. Записанный на диск токен пережил бы процесс,
который его выдал, а украденный каталог сервера стал бы набором живых
сессий.

Сессии различаются номером, а не адресом. Ключ по адресу вытеснял всех, кто
зашёл с одного IP, то есть всех игроков за одним NAT.

Тик-цикл: 20 раз в секунду, с ограничением догоняющих тиков. Мир, который
тикает по кадрам, тикает в зависимости от видеокарты — ровно то, от чего
мы уходим, перенося симуляцию на свой сервер.

Регистрация — команда оператора, а не функция у клиента. Открытая
саморегистрация превращает хранилище аккаунтов в спам-цель и в очередь
поддержки.

Проверено 12 сценариев, и главный из них — не юнит-тест, а настоящий
сокет: регистрация, вход, доказательство пароля, токен, его погашение и
появление игроком. Отдельно проверено, что неверный пароль отвергается, а
старое доказательство не принимается против нового вызова. Тик-цикл
замеряется числом тиков за секунду, а не наличием потока.

Всё одним JDK, без Gradle и без сети, как и весь проект.
2026-10-19 01:45:56 +03:00
sasheg dev bdf0ae866a Форк клиента собирается из исходников, модзагрузчик встроен в игру
Проект начинается с двух вещей: клиент, который мы действительно собрали, и
загрузчик, который является его частью, а не чем-то рядом.

Форк
- prepare-game.ps1 пересобирает клиент целиком: ванильный jar, SRG и MCP,
  SpecialSource, FernFlower, патчи, автофиксеры, javac. Идемпотентен.
- Свой код игры лежит в src-overlay, потому что src пересоздаётся при каждой
  сборке и всё написанное вручную там погибает.
- 116 патчей в patches/decompiler-fixes.txt. Каждый сверялся с оригинальным
  байткодом через javap: компиляция не ловит ошибки декомпиляции.
- struct_diff.py сравнивает исходный байткод с нашей сборкой по классам,
  интерфейсам, суперклассам, методам и полям, и отдельно проверяет, что
  пропавшие классы действительно никому не нужны.

Загрузчик
- Собирается и тестируется без игры: build.ps1 не использует classpath
  вообще, поэтому SelfTest из 128 проверок проходит без запущенного форка.
- Клиентский API не содержит ни одного игрового типа, реализацию даёт игра.
- Политика: мод меняет то, что клиент показывает, но никогда то, что истинно
  в мире. Мир, предметы, игроки и сеть принадлежат серверу.
- Capability приведены в соответствие с этой политикой: осталось шесть, и
  за каждым стоит реальная проверка. Прежние WORLD, PLAYER, NETWORK, TEXT,
  INPUT и COMMAND выдавались, но ничего не проверяли.
- Команды модов имеют область CLIENT, SERVER или BOTH. Серверные не
  выполняются на клиенте никогда.
- Свой jar загрузчика лежит в classpath игры и сборки форка.

Инструменты
- add_patch.py, чтобы каждое исправление переживало передекомпиляцию
- find_self_comparisons.py, find_remove_index_bugs.py, find_callers.py
- stage_run.py и fetch_missing.py для запуска клиента
2026-10-18 21:31:27 +03:00