- Протокол: Chat (что сказал сервер), ChatSend (что сказал клиент) и PlayerList
(весь список игроков с их режимами).
- Сервер: сообщения уходят всем, а имя отправителя ставит сервер, поэтому подделать
его нельзя. Список игроков уходит всем целиком, когда он меняется. Команды
сервера: /help и /players.
- Клиент: перевод наших пакетов в игровые (SPacketChat и SPacketPlayerListItem),
уходящий чат перехватывается патчем, список игроков обновляется по разнице
(ушедших убираем, пришедших добавляем).
- Тесты: два игрока, один пишет - второй слышит с правильным именем; список
игроков приходит обоим и меняется, когда кто-то выходит.
Попутно: run-server.ps1 теперь знает про rsx-plugin-api, иначе сервер не
запускался (нет класса Hitboxes в classpath).
Тесты: протокол 34, сервер 87. Форк собирается чисто.
Сервер умел принимать игроков и не умел ничего, что можно увидеть. Мир
был не написан: ни чанков, ни генерации, ни сохранения, ни игровых
пакетов.
Сделано:
- Пакет world: ChunkPos, Chunk, Blocks (теперь в протоколе), WorldGenerator,
ChunkStorage, World. Генерация чистая функция от зерна и координат, без
java.util.Random и без общего состояния, потому что чанк может
появиться через месяц после соседнего, на другой машине, и обязан
совпасть с ним побайтово. Сохранение — файл на чанк, запись во временный
файл и перенос, версия и контрольная сумма: половина записанного файла
это мир, который молча испортился.
- Снимок чанка и запись блока теперь под одним замком. Раньше копия могла
сняться посреди записи блока, и на диск уходила половина изменения.
- Player: позиция, вид, список отправленных чанков, проверка досягаемости
блока. Клиент не может переписать блок, до которого не дотянулся.
- Сервер держит мир, крутит его по тику, выкидывает давно не тронутые
чанки, шлет чанки вокруг игрока и рассылает изменения блоков и передвижения
тем, кто их видит.
- Клиентский сетевой слой в игре еще не тронут: это следующий шаг.
Проверено: 45 тестов сервера. Из них 25 на мир (детерминизм, круг
сохранения, автосохранение, вытеснение, отказ битому файлу) и 9 сквозных
через настоящий сокет: игрок входит, получает 25 чанков, чанк совпадает с
террейном сервера, блок ломается и остаётся сломанным после перезапуска
сервера, два игрока видят друг друга.
Теперь можно зарегистрировать аккаунт, поднять сервер и зайти в него.
Пароль не ходит по сети и не хранится на сервере в обратимом виде. Клиент
называет аккаунт, сервер отдаёт его соль и одноразовый вызов для этого
соединения, клиент тянет из пароля ключ через PBKDF2-HMAC-SHA256 и
подписывает им вызов вместе с именем. Пароль пересекает сокет ноль раз, а
запись обрезать нельзя: сервер не знает исходного пароля.
Отсюда важное следствие, которое сразу отозвало одну ошибку в протоколе:
соль обязана прийти раньше, чем клиент что-то доказывает, поэтому аккаунт
называется в Hello, а не в Auth. Иначе клиент получает соль не того
аккаунта и не может вывести ключ.
Учётная запись хранится как PBKDF2 соль, счётчик раундов и хеш. Раунды
лежат рядом с записью, а не в исходниках: число, существующее только в
коде, однажды становится причиной того, что аккаунт невозможно проверить.
Имена уникальны независимо от регистра: Player и player — это один
аккаунт. Иначе игрок получает двух персонажей и не понимает, какой его.
Троттлинг двумя ведрами, по аккаунту и по адресу: одно не мешает
подбирать одно имя, другое не мешает перебирать много имён с разных
адресов. Ответ на несуществующий аккаунт выполняет ту же работу, что и на
неверный пароль, и занимает то же время.
Токены живут только в памяти. Записанный на диск токен пережил бы процесс,
который его выдал, а украденный каталог сервера стал бы набором живых
сессий.
Сессии различаются номером, а не адресом. Ключ по адресу вытеснял всех, кто
зашёл с одного IP, то есть всех игроков за одним NAT.
Тик-цикл: 20 раз в секунду, с ограничением догоняющих тиков. Мир, который
тикает по кадрам, тикает в зависимости от видеокарты — ровно то, от чего
мы уходим, перенося симуляцию на свой сервер.
Регистрация — команда оператора, а не функция у клиента. Открытая
саморегистрация превращает хранилище аккаунтов в спам-цель и в очередь
поддержки.
Проверено 12 сценариев, и главный из них — не юнит-тест, а настоящий
сокет: регистрация, вход, доказательство пароля, токен, его погашение и
появление игроком. Отдельно проверено, что неверный пароль отвергается, а
старое доказательство не принимается против нового вызова. Тик-цикл
замеряется числом тиков за секунду, а не наличием потока.
Всё одним JDK, без Gradle и без сети, как и весь проект.