От объектов к данным
mcrs построен на Bevy ECS (entity component system). Если вы писали плагины для Paper или Minestom, почти все, что вы знаете о Minecraft, пригодится и здесь. Отвыкать придется от трех привычек, и эта страница разбирает их по очереди.
Игрок - это строка в таблице
Заголовок раздела «Игрок - это строка в таблице»В Paper игрок - объект. Вы держите Player, читаете его поля и вызываете методы:
for (Player p : world.getPlayers()) { if (p.getGameMode() == GameMode.CREATIVE) { log.info(p.getName()); }}В mcrs объекта игрока нет. Игрок - это Entity, а Entity - всего лишь id. Все, что сервер знает об игроке, лежит в компонентах, привязанных к этому id: в GameProfile хранятся имя и UUID, в PlayerGameMode - режим игры. С компонентами работает обычная функция, ее называют системой. Тот же цикл в виде системы выглядит так (это набросок, чтобы показать форму; в туториалах код взят из репозитория):
fn log_creative_players(players: Query<(&GameProfile, &PlayerGameMode), With<Player>>) { for (profile, mode) in &players { if mode.0 == GameMode::Creative { info!("{}", profile.username); } }}Проще всего представить базу данных. Сущность - строка, компонент - колонка, а система - запрос по всем строкам, где есть нужные ей колонки. Query из примера читается почти как SELECT profile, mode FROM entities WHERE has(Player).
В таблице ниже слева то, к чему вы привыкли в Java, справа - ближайший родственник в mcrs. Это аналогии для старта. Полная таблица соответствий есть на странице Paper и Minestom в терминах mcrs.
| Java | ECS в mcrs |
|---|---|
class Player extends LivingEntity |
id Entity с компонентами Player, GameProfile, PlayerGameMode и другими |
Поле player.gameMode |
Компонент PlayerGameMode на этой сущности |
Метод player.fly() |
Система, которая читает и пишет PlayerFlying |
for (Player p : world.getPlayers()) |
Query<(&GameProfile, &PlayerGameMode), With<Player>> |
| Ссылка на игрока в поле класса | id Entity в компоненте или ресурсе |
Bukkit.getServer() |
Параметры системы: Res<T>, Query<..>, Commands |
Метод Listener |
Наблюдатель (observer), функция вида fn(e: On<Event>), или система |
runTaskTimer |
Система в FixedUpdate, она выполняется 20 раз в секунду |
Данные вашего плагина живут по тому же правилу. Кладите их в компоненты и ресурсы, а если нужно запомнить игрока, сохраните id Entity и найдите игрока заново, когда он понадобится. Компоненты режима игры, полета и уровня оператора лежат в ability.rs.
Bukkit.getServer() здесь нет
Заголовок раздела «Bukkit.getServer() здесь нет»В mcrs нет статической точки входа. Система перечисляет в параметрах все, что ей нужно, и Bevy передает эти значения при каждом запуске: Res<T> для общего ресурса, Query<..> для сущностей, Commands для их создания и изменения.
Если вы работали с внедрением зависимостей, схема вам знакома. Сигнатура функции играет роль конструктора, а планировщик Bevy - роль контейнера. Заодно по сигнатуре и вам, и Bevy точно видно, что система читает и что пишет. На этом держится следующий раздел.
Главного потока тоже нет
Заголовок раздела «Главного потока тоже нет»Bevy сравнивает сигнатуры ваших систем. Если две системы работают с разными данными, они выполняются одновременно, и писать для этого ничего не надо. Если две системы пишут в одни и те же данные, возникает конфликт, и вы сами указываете, какая из них идет первой, через .before() или .after(). Bevy находит такие конфликты, когда строит расписание при запуске. Компилятор Rust их не ловит.
Вызывать Bukkit.getScheduler().runTask(...), чтобы вернуться в главный поток, не придется, потому что такого потока нет. От Folia эта модель тоже отличается. Folia делит мир на регионы по координатам, а mcrs делит работу по данным, с которыми работает каждая система.
Каждое измерение - отдельный мир, и измерения тикают параллельно. Время, погода и игровые правила у них общие, поэтому эти данные пишет один владелец, а каждое измерение читает их снапшот.1 Когда одному измерению что-то нужно от другого, оно отправляет сообщение.
Демо ниже прогоняет одно и то же стадо коров дважды: в порядке ванили и в порядке mcrs.
Одинаковый вход дает одинаковый результат
Заголовок раздела «Одинаковый вход дает одинаковый результат»К параллельному коду обычно один вопрос: не получатся ли из двух запусков два разных мира? В mcrs результат тика не зависит от того, сколько потоков его считало. Когда две сущности претендуют на одно и то же, победителя определяет стабильный ключ, например порядок прибытия или id сущности, и расклад по потокам на исход не влияет.
Насколько строга гарантия, зависит от части игры. В проекте три уровня:2
| Уровень | Гарантия | Примеры |
|---|---|---|
| Точный | Результат совпадает с эталонной игрой | Генерация мира, порядок блочных обновлений, арифметика движения игрока |
| Не зависит от планировщика | Одни и те же входные данные дают один и тот же тик при любом порядке потоков | Почти все остальное |
| Свободный | Порядок ни на что не влияет, потому что общих записываемых данных нет | Сущности и системы, которые пишут только в свои данные |
За пределами точного уровня mcrs повторяет распределения случайных чисел эталонной игры, но сами последовательности не воспроизводит.
Для плагина из этого следует одно правило. Если одна ваша система должна выполняться раньше другой, задайте это через .before() или .after() и не полагайтесь на порядок, который случайно увидели при запуске. CI запускает расписания симуляции так, что неоднозначный порядок считается ошибкой, поэтому неупорядоченный конфликт роняет тест.3 Тесты лежат в tick_determinism.rs и schedule_ambiguity.rs.
Паника не останавливает сервер
Заголовок раздела «Паника не останавливает сервер»Сервер ставит запасной обработчик ошибок в src/main.rs. Когда система паникует, обработчик пишет панику в лог, а сервер работает дальше. Это похоже на непойманное исключение в обработчике событий Bukkit.
Дальше: Rust для Java-разработчика.
Footnotes
Заголовок раздела «Footnotes»Источники и проверка
Проверено: 26.4-snapshot-7, 51d45c26a, 2026-11-09
Переведено с версии 51d45c26a