Вход в игре и скины
В игру игроки заходят через authlib-injector. Он направляет игру на Yggdrasil-endpoint Laminara, а тот проверяет логин через тот же источник аккаунтов, что и лаунчер, выдаёт игровые токены и подписывает скины.
Включается разделом yggdrasil:
{ "yggdrasil": { "enabled": true, "serverName": "Laminara", "rsaKeyPath": "/var/lib/laminara/yggdrasil-rsa.pem", "skinProvider": "template", "skinConfig": { "skin": "https://skins.example.com/%nickname%.png", "slim": true }, "skinDomains": ["skins.example.com"] }}serverName — то имя, которым сервер представляется authlib-injector. Игра показывает его при входе
и пишет в свои логи; ни на сборки, ни на лаунчер, ни на папки игрока оно не влияет. Установщик
подставляет туда название проекта, потому что в девяти случаях из десяти это одно и то же имя —
поменять можно в любой момент: laminara-server settings yggdrasil.serverName "Своё имя".
Yggdrasil поднимается на том же публичном порту, что и лаунчер, по пути /yggdrasil/. authlib-injector запускается с этим адресом:
-javaagent:authlib-injector.jar=https://ваш-домен/yggdrasil/Сам authlib-injector.jar скачивать не нужно: install кладёт его в корень сборки сам, сверив
контрольную сумму с сайтом проекта, и лаунчер подставляет агент при запуске. Файл общий для всех
платформ. Если положить туда свою версию, сервер её не тронет — так можно закрепить конкретную
сборку агента.
На игровом сервере
Всё, что выше, — про сторону игрока. Сам Minecraft-сервер по умолчанию проверяет входящих у Mojang и ваших игроков не пустит, поэтому его тоже нужно направить на Laminara. Делается это тем же агентом и один раз:
-
Положите
authlib-injector.jarрядом сserver.jar. Свежий — на странице релизов проекта; сгодится и тот, что Laminara положила в корень сборки. -
Допишите агент в команду запуска сервера перед
-jar, с тем же адресом, что и у лаунчера:Окно терминала java -javaagent:authlib-injector.jar=https://ваш-домен/yggdrasil/ -Xmx4G -jar server.jar nogui -
В
server.propertiesоставьтеonline-mode=true. Это не опечатка: агент подменяет не саму проверку, а того, у кого сервер спрашивает. Сonline-mode=falseсервер перестанет проверять вообще кого-либо, и под любым ником сможет зайти кто угодно. -
Впишите адрес сервера в настройки сборки —
serverAddress— и опубликуйте сборку заново. Тогда лаунчер покажет игрокам, сколько человек сейчас в игре.
Скины — тоже адаптеры
Откуда брать скины, задаёт skinProvider. Способа три: собрать ссылку по шаблону, спросить ваш
сайт по HTTP или прочитать колонки базы.
Из чего складывается ссылка
В адресах template и json работают подстановки. Для игрока Steve с
uuid 8667ba71-b85a-4004-af54-457a9734eed7 они дают:
| Подстановка | Во что превращается |
|---|---|
%nickname% | Steve |
%username% | Steve (то же самое, для привычки) |
%uuid% | 8667ba71-b85a-4004-af54-457a9734eed7 |
%hash% | 8667ba71b85a4004af54457a9734eed7 (uuid без дефисов) |
Ник берётся ровно в том написании, в котором он лежит в вашем источнике аккаунтов, а uuid — тот,
что Laminara выдала игроку (из колонки uuid, если вы её указали, иначе выведенный из ника).
Ссылка на файл собирается прямо из шаблона — ни база, ни ваш сайт при этом не опрашиваются.
Самый частый случай — файлы по нику:
"skinProvider": "template","skinConfig": { "skin": "https://skins.example.com/%nickname%.png"}https://skins.example.com/Steve.png
С плащами и тонкой моделью рук:
"skinConfig": { "skin": "https://skins.example.com/skins/%nickname%.png", "cape": "https://skins.example.com/capes/%nickname%.png", "slim": true}Если файлы разложены по uuid — так делают, когда ники разрешено менять:
"skinConfig": { "skin": "https://cdn.example.com/textures/%hash%.png"}https://cdn.example.com/textures/8667ba71b85a4004af54457a9734eed7.png
Годится и адрес с параметрами, если скины отдаёт скрипт:
"skinConfig": { "skin": "https://site.example/api/skin.php?nick=%nickname%", "cape": "https://site.example/api/cape.php?nick=%nickname%"}cape и slim необязательны. slim: true включает тонкую модель рук сразу для всех —
выбрать её игроку по отдельности этот способ не позволяет, для этого нужен json или sql.
Laminara запрашивает JSON у вашего эндпоинта и берёт ссылки из ответа. В URL работают те же подстановки:
"skinProvider": "json","skinConfig": { "url": "https://site.example/skins/%nickname%" }"skinConfig": { "url": "https://site.example/api/textures?uuid=%uuid%" }"skinConfig": { "url": "https://site.example/textures/%hash%.json" }Если ваш сервис отдаёт текстуры по ключу, добавьте заголовки — они уйдут с каждым запросом:
"skinConfig": { "url": "https://site.example/api/textures?username=%username%", "headers": { "Authorization": "Bearer ваш-секрет" }, "timeout": "10s", "cacheTTL": "1m"}Скины спрашиваются при каждом входе в игру, поэтому ответ помнится cacheTTL (по умолчанию
минуту) — ваш сервис не будет дёргаться на каждое подключение. cacheTTL: "0s" выключает
память совсем. В выводе settings значение заголовка с секретным именем скрывается.
Принимаются две формы ответа. Короткая — прямые ссылки на .png:
{ "skin": "https://site.example/skins/Steve.png", "cape": "https://site.example/capes/Steve.png", "model": "slim"}Минимально достаточный ответ — только скин:
{ "skin": "https://site.example/skins/Steve.png" }И формат GravitLauncher — чтобы уже написанный под него скрипт выдачи текстур работал без переделок:
{ "SKIN": { "url": "https://site.example/skins/Steve.png", "digest": "e3b0c44298fc1c149afbf4c8996fb924…", "metadata": { "model": "slim" } }, "CAPE": { "url": "https://site.example/capes/Steve.png", "digest": "b94d27b9934d3e08a52e52d7da7dabfa…" }}digest Laminara не требует и игнорирует — подлинность текстур обеспечивает подпись профиля.
Игрок без текстур — обычное дело, верните пустой объект:
{}Модель рук берётся из model (короткая форма) или metadata.model (форма Gravit); значение
slim включает тонкие руки, любое другое или его отсутствие — обычные.
Берёт ссылки прямо из таблицы сайта. Подходит, когда у каждого игрока свой адрес скина и шаблон не подставить.
Всё в одной таблице, поиск по нику:
"skinProvider": "sql","skinConfig": { "driver": "mariadb", "dsn": "laminara:пароль@tcp(127.0.0.1:3306)/site?charset=utf8mb4", "table": "users", "fields": { "username": "username", "skin": "skin", "cape": "cape" }}С колонкой модели — тогда тонкие руки включаются каждому игроку отдельно:
"skinConfig": { "driver": "mariadb", "dsn": "…", "table": "users", "fields": { "username": "username", "skin": "skin_url", "cape": "cape_url", "model": "skin_model" }}В колонке model значение slim даёт тонкие руки; пусто — обычные. Без этой колонки для всех
берётся slim из конфига.
Поиск по uuid вместо ника — если ники у вас меняются. В запрос уходит uuid с дефисами; если в вашей колонке он хранится без них, Laminara спросит второй раз уже без дефисов — подойдёт любая из двух форм:
"skinConfig": { "driver": "postgres", "dsn": "postgres://laminara:пароль@127.0.0.1:5432/site?sslmode=disable", "table": "users", "lookup": "uuid", "fields": { "uuid": "uuid", "skin": "skin", "cape": "cape" }}Если текстуры лежат отдельно от аккаунтов, задайте query вместо table и fields —
выборка возвращает скин, затем (необязательно) плащ и модель, а единственный параметр
подставляется по lookup:
"skinConfig": { "driver": "mariadb", "dsn": "…", "lookup": "uuid", "query": "SELECT skin, cape FROM textures WHERE uuid = ?"}Запрос может собирать ссылку на лету, если в базе лежит только имя файла:
"skinConfig": { "driver": "mariadb", "dsn": "…", "query": "SELECT CONCAT('https://cdn.example.com/skins/', file), NULL, model FROM textures WHERE nick = ?"}driver — mariadb, mysql, postgres или sqlite. В postgres параметр пишется как $1,
в остальных — ?. Пользователю базы достаточно SELECT на эти колонки: записывать Laminara
ничего не будет. Игрок без скина не считается ошибкой — профиль просто отдаётся без текстур.
skinDomains перечисляет домены, с которых игра примет текстуры. Домены ваших скинов должны быть в этом списке.
Сервер раздаёт картинки сам
Игра узнаёт скачанную текстуру по адресу и больше за ней не ходит: файл ложится в её кэш навсегда.
Поэтому скин, который лежит по неизменному адресу вроде https://skins.example.com/Steve.png, после
замены картинки так и останется старым — сколько игру ни перезапускай. Модель рук при этом меняется
сразу, потому что она приходит в самом профиле, а не картинкой.
Чтобы этого не случалось, Laminara не отдаёт игре ваш адрес напрямую. Сервер скачивает картинку, считает её хеш и публикует у себя:
https://ваш-домен/yggdrasil/textures/<хеш картинки>Сменилась картинка — сменился хеш, а с ним и адрес: игра видит новый файл и скачивает его. Так же
устроены адреса скинов у самого Mojang. Отсюда две приятные мелочи: в skinDomains достаточно
домена самого сервера (Laminara добавляет его туда сама), а ваш сайт со скинами перестаёт держать
нагрузку от игроков — к нему ходит одна Laminara, не чаще раза в минуту на игрока.
Публичный адрес зеркало берёт из launcher.endpoints — первого в списке; без него зеркало не
включается. Картинка должна открываться с самого сервера Laminara: если она не открылась или по
адресу лежит не PNG, зеркало отступает и отдаёт игре исходную ссылку — тогда домен вашего сайта
обязан быть в skinDomains. Выключается зеркало целиком:
"yggdrasil": { "mirrorTextures": false}Скин в одиночной игре
Скин виден не только на сервере. Клиент сам спрашивает профиль игрока по его uuid у того же
/yggdrasil/ и рисует по нему себя в одиночном мире — запроса к игровому серверу тут нет вовсе.
Laminara отвечает на такой запрос теми же текстурами, что уходят игровому серверу.
Профили игроков лежат рядом с игровыми сессиями: в redis, если он настроен в auth.sessions, —
и тогда перезапуск сервера их не теряет. Игрока, о котором сервер ещё не слышал, профиль
отдаётся пустым — это видно как «в одиночке скина нет, и к источнику скинов не приходит ни
одного запроса». Лаунчер продлевает игровую сессию при каждом запуске, так что после следующего
старта лаунчера скин появляется сам.
Голова в лаунчере
Лаунчер показывает лицо игрока в шапке рядом с ником. Берёт он его из того же профиля, что и игра в одиночном мире: скачивает картинку скина и вырезает лицо вместе со вторым слоем (шлем, волосы). У старых скинов 64×32 сплошь непрозрачный слой шлема не рисуется, как и в самой игре.
Лицо обновляется при каждом запуске лаунчера и когда игрок возвращается в его окно, но не чаще раза в две минуты. Сменил скин на сайте, переключился в лаунчер, и голова уже новая. Если у игрока скина нет или профиль пустой, в шапке остаётся первая буква ника.
Подпись текстур
В корне /yggdrasil/ сервер публикует RSA-ключ — он создаётся и сохраняется по rsaKeyPath при первом запуске. Свойство текстур каждого профиля подписывается этим ключом по схеме RSA-SHA1, ровно так, как проверяет authlib-injector. Поэтому игра доверяет скинам.