The Jerusalem PostHow Israel hopes third war with Iran will be different than Gaza - analysisDaily MaverickWhen baboon captivity becomes ‘ethical’ modern conservation, something has gone terribly wrongCollider‘Homestead: The Series’ Officially Returns After 2 Years Hiatus With an Epic First LookDeadlineKatey Sagal To Recur On ‘Best Medicine’ For Season 2Anime News NetworkCrunchyroll Adds Gunma-chan Anime's English DubVarietyVariety’s ‘A Night in the Writers’ Room: Awards Season Edition’ Returns for 2026ABC News (Australia)Airline damages $33k wheelchair belonging to girl with rare bone diseaseRMF24Kiedy pociągi przyspieszą do 250 km/h? Zapytamy wiceministra infrastrukturyZDF heuteAktuelle Pressemitteilungen des ZDFBBC NewsLabour ends power-sharing with Greens in 11 councils over Zionism rowIl Sole 24 OreVida e Iva digitale: cosa cambia dal 2027 per i sistemi gestionaliScreen RantPercy Jackson Season 3 Officially Unveils 2 Major Missing Gods’ Debuts
The Daily Newsstand · Free, Always
Monday, November 2, 2026

Хотел отправить два состава на турнир — сделал мод для Esports Manager 2026

Translate

У меня в Esports Manager 2026 выросла академия. Причём выросла до состояния, когда это уже полноценная топ-команда: основа занимает первое место в рейтинге, академка — шестое. Хочется отправить обе на сильный турнир и посмотреть, что получится. Но игра предлагает выбирать.

Меня это бесило. Выращиваешь второй состав, вкладываешься в него, а когда он становится конкурентоспособным, упираешься в ограничение на участие вместе с основой. Так появился мой первый игровой мод — Dual Roster.

В итоге оба состава действительно встретились в финале верхней сетки. Академия даже забрала карту у основы. Но между «разрешить две заявки» и этим матчем оказались регистрация, письма, расходы на поездки, интерфейс и загрузка обновлений из Steam Workshop.

Расскажу, как это устроено: что вообще делает Workshop, на чём пишутся моды, как C#-плагин меняет поведение Unity-игры и почему одной новой кнопки недостаточно.

Исходники на GitHub · Мод в Steam Workshop

Что получилось со стороны игрока

Dual Roster позволяет отдельно заявлять основу и академию на один турнир. У каждого состава остаются свои условия допуска: наличие игроков, подходящий рейтинг и отсутствие занятости в другом турнире. Мод снимает конкретное ограничение на совместное участие, сохраняя эти проверки.

На странице турнира можно подать или отменить заявку каждого состава. В письме-приглашении доступны три действия: заявить основу, заявить академию и отказаться. Если турнир LAN, перелёт и гостиница оплачиваются отдельно из бюджетов составов. В разделе цифровых товаров добавлено переключение между основой и академией.

Письмо-приглашение с отдельными действиями для основы и академии

Письмо-приглашение с отдельными действиями для основы и академии

Внизу письма — состояние обоих составов и три действия. Выбор академии больше не занимает место действия «отказаться».

При этом сама игра по-прежнему ведётся от лица основы. Управлять академией в отдельном матче мод не позволяет: она участвует в симуляции как самостоятельная команда. Для моей задачи этого достаточно.

Дальше речь о версии 0.3.0 beta для Windows x64 и Steam build 25869291. Привязка к сборке здесь существенна: мод обращается к внутренним типам и методам игры.

Что Steam Workshop делает с модом

У игрока Workshop выглядит просто: нажал «Подписаться», дождался загрузки, запустил игру. За этой последовательностью легко не заметить два отдельных механизма.

Steam доставляет файлы, поддерживает подписки и обновления. А загрузить содержимое и применить его должна игра либо подключённый к ней загрузчик. Формат пакета и способ его использования определяются конкретной интеграцией. Это следует и из документации Steamworks по Workshop.

Поэтому универсального «языка модов Steam» нет. Возможны разные варианты:

Что меняется

Что может лежать в моде

Кто применяет изменения

Значения характеристик, переводы, описания

Таблицы и файлы данных в формате игры

Загрузчик данных игры

Карты, модели, текстуры, звук

Ресурсы, собранные подходящими инструментами

Система ресурсов или официальный редактор

Поведение через предусмотренный API

Скрипты на поддерживаемом игрой языке

Встроенная среда исполнения

Внутренняя логика и интерфейс

Программный плагин

Среда моддинга и механизм перехвата вызовов

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

Dual Roster относится к последнему варианту. В пакете находится исполняемый плагин, а для его подключения нужен первоначальный bootstrap — однократная установка среды и небольшого загрузчика. Одна подписка этот шаг не заменяет.

Стек: C#, Unity, IL2CPP, BepInEx и Harmony

Основной код мода написан на C#. В проекте указан TargetFramework со значением net6.0. Для сборки по инструкции репозитория используется .NET SDK 10; версия SDK и целевая платформа проекта — разные параметры.

Игра использует Unity и IL2CPP. Unity описывает этот backend так: управляемые сборки преобразуются в C++, после чего компилируются в нативный код для целевой платформы. Поэтому представление «открою исходный C# игры и поменяю условие» здесь слишком упрощённое. Подробнее — в документации Unity.

В проекте слои распределены так:

Компонент

Роль в Dual Roster

BepInEx для Unity IL2CPP

Загружает плагины и предоставляет им окружение

Il2CppInterop

Связывает управляемый код плагина с IL2CPP-типами игры

Обёртки игрового API

Дают типы вроде DataTeam, DataTournament, TournamentEngine

Harmony

Подключает обработчики к выбранным методам

Unity UI и TextMeshPro

Отображают дополнительные элементы интерфейса

Steam UGC API

Позволяет загрузчику узнать состояние подписки и файлов Workshop

Обёртки не означают, что разработчики выдали официальный SDK с обещанием стабильности. В данном случае код зависит от внутреннего устройства конкретной сборки. Структура проекта и ссылки на библиотеки видны в DualRoster.csproj.

Есть и менее заметная часть: пакет использует модифицированный Il2CppInterop.Runtime. В BUILD.txt указано изменение описания структуры класса для Unity 6000.5 в Class_6000_5.cs; исходники этой версии библиотеки приложены отдельно. Поэтому для воспроизводимой сборки недостаточно взять произвольный набор DLL с похожими названиями: нужна совместимая среда из поддерживаемого пакета.

Сам плагин наследуется от BasePlugin. В его Load() после проверок совместимости подключаются патчи и компонент интерфейса:

new Harmony(Id).PatchAll(typeof(Plugin).Assembly);
AddComponent<RosterPanel>();

PatchAll находит объявления патчей в сборке. Дальше нужные обработчики выполняются при обращении игры к соответствующим методам. Полный вход в плагин находится в Core.cs.

Откуда берётся запрет на второй состав

Важный объект здесь — ключ организации при регистрации. Если основа и академия считаются одной организацией, второй состав воспринимается как повторная заявка этой организации.

В моде для команд игрока ключом становится собственный Id состава. Для остальных команд сохраняется группировка академии через OriginTeamId. Вот соответствующий метод из исходников:

internal static string SeatKey(DataTeam? team)
{
    if (team == null) return "";
    if (IsPlayerTeam(team)) return team.Id ?? "";
    return team.IsAcademy && !string.IsNullOrEmpty(team.OriginTeamId)
        ? team.OriginTeamId : team.Id ?? "";
}

Здесь есть важное ограничение области действия: изменение касается клуба игрока. Мод не объявляет все академии мира независимыми организациями и не перестраивает весь состав участников турниров по новому правилу.

Для подключения этого поведения используется Harmony Prefix:

[HarmonyPatch(typeof(TournamentEngine),
    nameof(TournamentEngine.GetOrgRegistrationKey))]
internal static class SeatKeyPatch
{
    static bool Prefix(DataTeam team, ref string __result)
    {
        if (!Rules.IsPlayerTeam(team)) return true;
        __result = Rules.SeatKey(team);
        return false;
    }
}

В этом обработчике true разрешает исполнение оригинального метода. Для команды игрока результат задаётся через __result, а false пропускает оригинал. Это стандартная семантика Prefix в Harmony.

Однако ключ проверяется в нескольких местах. Помимо получения ключа патчится занятие места — TryClaimOrgSeat, — а также выбор команд без повторяющихся организаций, PickTeamsWithoutOrgDuplicates.

В исходниках отдельно отмечена причина последнего патча: нативный компилятор встроил вычисление ключа в метод отбора. Изменение одного вспомогательного метода не меняет уже встроенную логику. Поэтому отбор переопределён отдельно, с сохранением порядка команд, параметра skip и ограничения targetCount.

Это характерная техническая деталь моддинга скомпилированной игры: найденный метод может быть правильным по смыслу, но не покрывать все фактические пути исполнения.

Разрешить участие — ещё не значит разрешить всё

Проверка CannotEnter последовательно отвечает на несколько вопросов: регистрация ещё открыта? Состав существует и не расформирован? Он уже заявлен? В нём есть пять игроков? Он проходит по рейтингу? Не занят ли в другом турнире?

Рейтинг проверяет штатный TournamentEngine.CanParticipateInTournament, занятость — DataTournaments.IsTeamActiveInAnyTournament. Мод использует уже существующие правила там, где их не требуется менять.

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

Повторная проверка нужна даже в одиночной игре. Открытое письмо может устареть относительно состояния турнира. Недоступная кнопка сама по себе не должна быть единственным местом, которое удерживает модель данных в допустимом состоянии.

Для подачи заявки вызывается штатный PopupCreator.Instance.RegisterForTournament, для снятия — TournamentEngine.Instance.UnregisterPlayer. Это позволяет встроиться в существующий сценарий вместо создания параллельного списка участников, о котором остальная игра ничего не знает.

Почему понадобились собственные элементы интерфейса

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

На странице турнира RosterPanel создаёт отдельный Canvas в режиме ScreenSpaceOverlay. У него sortingOrder = 100, GraphicRaycaster для обработки взаимодействий и CanvasScaler с опорным разрешением 1920×1080. Надписи — TextMeshProUGUI, кнопки — обычные UnityEngine.UI.Button.

Панель видна только на активной странице турнира в состоянии регистрации. Статусы обновляются с интервалом 0,25 секунды. Если панель не удалось создать, Bind возвращает ошибку; на этом пути сохраняются штатные элементы управления.

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

Третья кнопка создаётся клонированием штатной. У клона очищается событие клика:

_decline = UnityEngine.Object.Instantiate(
    view._cancelBtn, view._cancelBtn.transform.parent);
_decline.gameObject.name = "DualRosterDeclineInvitation";
_decline.onClick = new Button.ButtonClickedEvent();

Это не косметика. Вместе с объектом можно скопировать и привязанные обработчики. У нового элемента должно быть собственное действие, а не поведение исходной кнопки в дополнение к новому.

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

Письмо теперь описывает турнир, а не одну заявку

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

В Dual Roster активность письма вычисляется по обоим составам. Пока регистрация открыта, приглашение не отклонено и хотя бы один состав может участвовать, действие остаётся доступным.

У отказа другая семантика. Он закрывает оба приглашения на этот турнир, но сохраняет уже поданные заявки. Снять состав можно отдельно на странице турнира. Так нажатие «Отказаться» после регистрации основы не отменяет сделанный выбор неожиданным образом.

Состояние отказа хранится через маркер в BodyAfterContent письма. Это поле входит в обычное сохранение письма, поэтому отказ переживает загрузку карьеры. При отказе мод находит входящие приглашения на тот же турнир, помечает их и деактивирует штатным вызовом.

У подхода есть компромисс: состояние связано с текстовым полем и конкретной строкой-маркером. Зато ему не нужен отдельный файл настроек, который придётся синхронизировать с копиями карьеры. Код этой части находится в EmailPatches.cs.

Два состава должны оплачивать две поездки

После регистрации появляется следующий вопрос: кто платит за перелёт и гостиницу?

В TravelPatches.cs мод определяет составы, которые едут вместе на активный LAN-турнир. Когда заявлены оба, штатная операция оплаты вызывается для каждого. Во время этого вызова хранится контекст PayingTeam.

Этот контекст решает две задачи. Во-первых, позволяет направить расход в бюджет нужного состава через DataFinances.LedgerOfUserSquad(team). Во-вторых, служит защитой от повторного входа: вызванный из патча штатный метод тоже проходит через патч.

if (PayingTeam != null) return true;

При вложенном вызове оригиналу разрешается работать. Снаружи контекст устанавливается перед оплатой и очищается в finally, в том числе при исключении.

Исправление проводки ограничено типами TICKETS и HOTEL и действует только внутри контекста поездки. Зарплаты и остальные расходы не должны внезапно начать подчиняться правилу «списать с двух составов». В описание операции и связанные письма добавляется название команды, чтобы расходы можно было различить.

Полная реализация доступна в TravelPatches.cs.

Доход академии должен быть виден отдельно

В разделе цифровых товаров появился переключатель составов. Он меняет отображаемые наборы, показатели продаж и доходы от автографов игроков выбранной команды.

Технически выбор привязан к конкретному экземпляру финансового раздела и клубу. При переключении сбрасывается флаг построения интерфейса _built, затем вызывается Fill().

На время заполнения устанавливается контекст выбранной команды. Патч SetsOf подставляет её в аргумент, после чего контекст восстанавливается через Finalizer. Таким образом, финансовая страница может показать академию без глобального переключения GlobalValues.DataTeam.

Это существенно: глобальное состояние клуба читают другие части игры. Для просмотра одной страницы менять его слишком широко. Здесь выбор ограничен синхронным заполнением интерфейса. Код — в StickerDashboard.cs.

Загрузчик: от подписки до работающего плагина

В проекте две сборки: DualRoster.Workshop.dll и DualRoster.dll. Первая остаётся установленным загрузчиком, вторая содержит игровую функциональность и приходит через Workshop.

Последовательность выглядит так:

Запуск игры
  → BepInEx
  → DualRoster.Workshop.dll
  → проверка Steam и подписки
  → ожидание готового пакета Workshop
  → проверка манифеста и контрольной суммы
  → копирование проверенной DLL в кеш
  → загрузка DualRoster.dll
  → подключение игровых патчей
  → загрузка карьеры

Загрузчик обращается к Steam UGC API: GetItemState сообщает состояние пакета, GetItemInstallInfo — расположение установленных файлов, DownloadItem позволяет запросить загрузку. В данном проекте разрешён один конкретный Workshop ID.

Наличие файлов на диске ещё не означает готовность. Пользователь мог отписаться, а старая копия осталась в кеше. Или пакет установлен, но Steam уже скачивает обновление. Поэтому состояние проверяется отдельно:

internal static PackageState State(uint flags)
{
    if ((flags & 1) == 0) return PackageState.Unsubscribed;
    // Installed=4, NeedsUpdate=8, Downloading=16, DownloadPending=32.
    return (flags & 4) != 0 && (flags & (8 | 16 | 32)) == 0
        ? PackageState.Ready : PackageState.WaitingForDownload;
}

Затем читается манифест: проверяются версия схемы, API загрузчика, идентификатор пакета, совместимость и формат версии. Контрольная сумма DLL сверяется с Sha256 в манифесте.

Хеш здесь проверяет соответствие файла манифесту. Это не цифровая подпись автора: если заменить и DLL, и манифест, одна такая проверка подлинность не установит.

Проверенные байты записываются в кеш, из которого плагин и загружается. Благодаря этому Steam может заменить файлы пакета, пока игра работает. Текущая сессия продолжает использовать уже загруженную версию.

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

Обе части можно посмотреть отдельно: Loader.cs и WorkshopPackage.cs.

Почему обновление игры может сломать мод

Перед подключением патчей проверяется SHA-256 файла GameAssembly.dll. Если он отличается от ожидаемого, загрузка прекращается с сообщением о поддерживаемой сборке.

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

Есть и зависимость от загрузки сохранения. В Core.cs отключается штатная миграция MigrateAutoSeatedUserAcademy, которая, согласно комментарию в коде, удаляет вручную зарегистрированную академию игрока. Мод не выставляет флаг завершения этой миграции. После его удаления стандартная обработка снова может сработать.

Поэтому «отписаться от мода» и «вернуть карьеру в состояние до мода» — разные операции. Перед установкой нужна копия сохранения, а после обновления игры — проверка совместимости. Для этой версии нет заявления о поддержке любых последующих сборок или произвольных сочетаний с другими модами.

Что проверено и что произошло в моей карьере

В репозитории есть независимые проверки пакета Workshop и интеграционные проверки внутри игры. По отчёту версии 0.3.0, выполнено 119 автоматических проверок: 62 для регистрации, приглашений и отказа, 35 для финансов и интерфейса цифровых товаров, 22 для пакета и состояний загрузки Workshop.

Тестовый режим использует отдельное хранилище сохранений. Для него требуется маркер .dualroster-test-copy, а пути MessagePackStorage.PATH и JSONStorage.PATH перенаправляются в тестовую копию. Это важно, потому что проверки действительно меняют карьеру.

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

Отдельно я дождался самого интересного сценария в своей карьере. Основа и академия прошли по сетке Hyperion Series и встретились в финале верхней сетки, в формате BO3.

Оба состава в сетке Hyperion Series

Оба состава в сетке Hyperion Series

До очной встречи команды прошли разные ветки. Академия победила NAVI 2:1, основа — G2 2:0.

Перед матчем основа занимала первое место в рейтинге с 2034 очками ERS, академия — шестое с 1663.

Академия против основы перед финалом верхней сетки

Академия против основы перед финалом верхней сетки

Я выбрал быстрый матч: обычно играю именно через симуляцию. Основа победила 2:1.

Результат быстрой симуляции: академия 1, основа 2

Результат быстрой симуляции: академия 1, основа 2

Это подтверждает конкретный сценарий: два состава попали в один турнир, встретились между собой, быстрая симуляция завершилась результатом. Полноценный матч с ручным управлением в этой проверке я не запускал. Сам мод не добавляет возможность играть за академию.

Зато исходная цель достигнута. Академка, ради которой всё затевалось, оказалась достаточно сильной, чтобы отобрать карту у основы.

Как попробовать и куда смотреть в исходниках

Для версии 0.3.0 порядок такой:

  1. Подписаться на Dual Roster в Steam Workshop и дождаться скачивания.

  2. Закрыть игру и сделать резервную копию карьеры.

  3. Открыть папку пакета SteamLibrary/steamapps/workshop/content/2749950/3819071177/ и запустить Install.cmd.

  4. Запустить игру через Steam и дождаться в главном меню статуса Dual Roster: 0.3.0 active / 0.3.0 включён.

  5. После этого открыть карьеру.

Обычные обновления игровой DLL подхватываются загрузчиком при следующем запуске. Обновление самой игры или среды совместимости может потребовать новой установки — актуальные условия указаны в README и описании пакета.

Для сборки по инструкции репозитория нужны установленный поддерживаемый пакет, игровые API-обёртки и .NET SDK. В корне есть Build.ps1; отдельные проверки пакета запускаются командой:

dotnet run --project tests/WorkshopChecks.csproj -c Release

Это проверки загрузчика и пакета, а не замена запуску интеграционных сценариев внутри игры. Подробности сборки находятся в BUILD.txt.

Структура небольшая: src/ содержит игровую логику и интерфейс, loader/ — подключение Workshop-пакета, installer/ — установку, tests/ — независимые проверки. Собственный код опубликован под MIT; условия сторонних компонентов вынесены отдельно в THIRD-PARTY.txt и third-party/.

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

GitHub: код, сборка и ограничения · Steam Workshop: установка мода

А какое небольшое ограничение в игре раздражало вас настолько, что хотелось сделать собственный мод?

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.