Multi-version совместимость

Версия Minecraft
1.21+
API
Fabric
В общем изначально я делал мод под 1.21. Далее опытным путем выяснил, что текущая версия мода так же подходит и для 1.21.1. Но на 1.21.2 другие маппинги (я использую Yarn), а если указать совместимую версию маппингов – ломается код.

Я бы хотел, реализовать совместимость с другими версиями, но похоже для этого придется переписывать код. Следовательно, если в будущем я захочу добавить новый функционал, его придется писать под каждую реализацию, под каждую версию?

Я слышал, что можно сделать свой интерфейс, который реализуется под каждую версию, таким образом придется переписывать меньше кода, но я трудно представляю как это сделать.
Так же хотелось бы получить единый jar файл, который будет работать на нескольких версиях, с разными маппингами. Не на всех, но хотя бы на 1.21-1.21.11 Возможно ли это?

В общем вопрос в следующем:
1. Каким образом хранить реализации мода для версий, что бы ничего не утерялось и не сломалась реализация? Не делать же новый проект для каждой.
2. Существует ли вообще какое то готовое решение, что бы избежать всего этого геморроя?
 
Поздравляю, вы столкнулись с проблемой враждебности игры к моддингу.
У Minecraft нет чёткой устоявшейся архитектуры; постоянно что-нибудь меняется туда-сюда; вы никогда не знаете что вам "приготовит" Mojang в новом релизе – им глубоко всё равно на то что там будет с модами.

Я слышал, что можно сделать свой интерфейс, который реализуется под каждую версию, таким образом придется переписывать меньше кода, но я трудно представляю как это сделать.
Даже если вы это сделаете, хватит его всего на несколько релизов игры, а потом снова переписывать.

Так же хотелось бы получить единый jar файл, который будет работать на нескольких версиях, с разными маппингами. Не на всех, но хотя бы на 1.21-1.21.11 Возможно ли это?
Загрузчики модов должны уметь работать с таким волшебным Jar. С вашей стороны проще систему сборки настроить так чтобы она выдавала несколько Jar под разные версии игры.

1. Каким образом хранить реализации мода для версий, что бы ничего не утерялось и не сломалась реализация? Не делать же новый проект для каждой.
Иметь несколько веток в Git. С помощью cherry-pick копировать новые фичи из одной в другую.
 
Я бы хотел, реализовать совместимость с другими версиями, но похоже для этого придется переписывать код.
То что ты описываешь - серьезная архитектурная задача. По сути у тебя может быть как минимум несколько факторов, множащие твои таргеры сборки:
1) Версия игры
2) Модлоадер
3) Маппинги (имхо, я не вижу практической пользы от поддержки кастомных маппингов, но оно тут присуствует т.к. ты явно описал этот реквайремент)

Я слышал, что можно сделать свой интерфейс, который реализуется под каждую версию
Да, это классический прием. Ты описываешь верхнеуровневую логику твоего мода, но со специфичными штуками вроде апишек модлоадера, версии игры и маппингов ты взаимодействуешь через абстракции (интерфейсы, абстрактные классы, лямбды etc). В итоге вся твоя логика будет изолирована от третьесторонних факторов описанных выше. Ты получишь ту самую универсальность о которой говоришь.

Следовательно, если в будущем я захочу добавить новый функционал, его придется писать под каждую реализацию, под каждую версию?
Верно. На твои плечи ложится мейнтейнинг имплементаций этих самых абстракций. И это довольно дорого. Если подсчитать на примере, когда тебе нужно поддерживать 5 версий игры, 3 модлоадера и 2 маппинга, то суммарно тебе придется предоставить 30 имплементаций на каждую комбинаций этих факторов.

Плюс стоит отдавать себе отчет, что проектируя такой слой абстракции, ты фактически пилишь свое универсальное моддинг апи. И чем больше умеет твоя абстракция, тем больше вероятность что что-то будето нельзя реализовать на одной из целевых платформ в силу их собственных ограничений.

НО! Справедливо полагать что ты можешь переиспользовать одну имплементацю в другой при отсуствии breaking changes между ними. И тут начинаются невероятные танцы. Ты буквально пытаешься выделить имплементации в отдельные модули, чтобы реиспользовать их среди других. И само собой что в новых версиях могут прилететь изменения, которые сломают такие модули и их придется дробить.

Существует ли вообще какое то готовое решение, что бы избежать всего этого геморроя?
В переиспользовании имплементаций может так же помочь Stonecutter (по сути препроцессор, умеющий вырезать код по условию версии) и Architectury API (универсальное моддинг апи позволяющая спратать несколько имплементаций в одну)

Так же хотелось бы получить единый jar файл, который будет работать на нескольких версиях, с разными маппингами.
Это еще сильнее усложняет твою хотелку. Единственный мод, который на моей памяти реализует такое - zume.

Резюмируя - все это можно сделать, но стоимость проектирования, поддержки, тестирования и найма девелоперов необходимого уровня будет очень и очень высокой.
 
Иметь несколько веток в Git. С помощью cherry-pick копировать новые фичи из одной в другую.
Это значительно усложняет flow релизов и в значительной мере подвержено человеческим ошибкам. Представляю лицо несчастного, которому нужно таким образом релизунуть скажем, апдейт под 10 таргетов.

Гораздо более человеколюбиво в рамках одной ветки иметь по модулю, ответственному за сборку в определенный таргет. Оборачиваешь вызов каждого в CI-пайплайн и релизишься как человек.
 
Для мультиверсионных модов помимо уже упомянутого Stonecutter есть ещё один менее популярный инструмент Cloche, он берёт код сразу со всех версий и склеивает из них общий jar, оставляя только код который присутствует на всех версиях сразу, а тот что отличается между версиями пишется уже в отдельных подпроектах. Для порта между схожими версиями вроде 1.21.1-1.21.4 как будто довольно удобно, но для условной поддержки 1.19.2 + 1.20.1 + 1.21.1 как по мне Stonecutter всё же удобнее за счёт всякого ремаппинга и замены строк/констант.

А вот делать общий jar с несколькими версиями и модлоадерами я бы не советовал, у Forge и Fabric маппинги разные, между версиями тоже код отличается, по итогу придётся дублировать код с разными маппингами и в зависимости от версии запускать нужную реализацию, но это раздувает итоговый размер в несколько раз. Как альтернативу, можно тащить деобфусцированный jar и обфусцировать его в рантайме при первом запуске пропихивая в игру через кастомный класслоадер, но там тоже свои сложности и как по мне усилия не стоят результата 🙃
 
Я не так давно пробовал сделать свою систему схожую с Cloche, только наоборот, взять код со всех версий и слить в общий jar и через плагин для IDEA (по аналогии с линтером в Android) уже проверять код под конкретную версию. В целом тема рабочая и на первый взгляд удобная:
1775585692309.png1775585698819.png
Даже в Completion Contributor сразу пометки об версии метода / переменной :D
1775585702344.png

Но когда дело дошло до более редких случаев, вроде изменения возвращаемого типа или generic-параметров или когда на одной версии один конструктор, а на другой - уже с другими параметрами задумка просто посыпалась, лучшее к чему я пришёл - генерировать для таких случаев синтетические имена вроде registryAccess$1_20_1/1_21_1, но как по мне тут уже мороки местами даже больше, чем было со Stonecutter, так что без правок в компилятор такое делать смысла не вижу, а это долго и толком никому не надо
 
Назад
Сверху