Script Container Extensions
Как регистрировать свои сервисы и подключать модульные библиотеки в скриптах EyeAuras
Script Container Extensions
ScriptContainerExtension это способ зарегистрировать в DI-контейнере скрипта свои сервисы, helper-классы и API.
Если говорить совсем просто:
- обычный
GetService<T>()получает уже зарегистрированный сервис ScriptContainerExtensionпозволяет добавить новые регистрацииAddNewExtension<T>()это способ подключить такую extension во время работы скрипта
Эта тема особенно полезна, если вы:
- выносите код в отдельные классы и библиотеки
- делаете свой mini-app или крупный pack
- подключаете модульные пакеты вроде ImGui или Frida
- хотите, чтобы ваши helper-классы создавались через DI, а не через
new
Зачем это нужно
У EyeAuras уже есть sandbox и базовые сервисы. Но иногда вам хочется добавить в контейнер что-то свое:
IMyHelper -> MyHelperIBotState -> BotStateIFridaExperimentalApi -> FridaExperimentalApiIImGuiExperimentalApi -> ImGuiExperimentalApi
Для этого и нужен ScriptContainerExtension.
Без него вы часто упретесь в один из двух сценариев:
- придется вручную создавать граф объектов через
new - или нужный сервис вообще не будет известен контейнеру
Где это находится в архитектуре
У каждого скрипта есть sandbox и связанный с ним DI-контейнер.
Также внутри есть IScriptingApiContext. Это внутренний объект контекста, который хранит:
- Roslyn
Solution - контейнер
- версию workspace
Anchorsдля ресурсов этого контекста
Обычно вы не создаете IScriptingApiContext вручную. Но полезно понимать идею: container extensions расширяют именно этот слой сервисов, из которого потом работает GetService<T>().
Самое важное: Script.csx и обычные классы
В Script.csx вы можете прямо писать:
Log.Info("Hello");
var helper = GetService<MyHelper>();
А вот MyHelper это уже обычный C# класс. У него нет прямого доступа к Log, AuraTree, GetService<T>() и другим возможностям sandbox, если вы сами их не передали.
Поэтому container extensions особенно полезны для связанных классов: они позволяют создавать такие классы через DI и автоматически получать зависимости в конструктор.
Реалистичный пример из игрового бота
В реальном боте extension обычно регистрирует не "счетчик", а целый набор сервисов.
Типичный пример:
IEntityManagerхранит всех объектов вокруг персонажа, текущую цель, кэш сущностей и результаты чтения памятиIGeodataManagerотвечает за геодату и навигациюIBotBrainпринимает решения на основе уже готовых сервисов- UI может жить в ImGui или Blazor и читать те же самые данные
Упрощенный пример такой extension может выглядеть так:
public sealed class BotContainerExtensions : ScriptContainerExtension
{
protected override void Initialize()
{
Container.AddNewExtensionIfNotExists<ImGuiContainerExtensions>();
Container.AddNewExtensionIfNotExists<FridaContainerExtensions>();
Container.RegisterSingleton<IEntityManager, EntityManager>();
Container.RegisterSingleton<IGeodataManager, GeodataManager>();
Container.RegisterType<IBotBrain, BotBrain>();
Container.RegisterType<ILoginForm, ImGuiLoginForm>();
}
}
Почему это хороший пример:
IEntityManagerимеет смысл держать какSingleton, потому что это единый источник правды для всего бота- UI, логика боя, pathing и debug-инструменты могут читать одни и те же данные из одного менеджера
IBotBrainчасто удобнее регистрировать черезRegisterType, если вы хотите создавать отдельный экземпляр под конкретную задачу
Почему IEntityManager это типичный singleton
Если говорить игровыми терминами, EntityManager обычно отвечает за вещи вроде:
- список объектов вокруг персонажа
- текущая цель
- игрок
- кэш мобов, NPC, лута
- обновление снимка мира для других подсистем
Такой сервис почти всегда хочется иметь один на весь бот, а не создавать заново в каждом helper-классе.
Если сделать несколько отдельных EntityManager, очень легко получить:
- несколько несогласованных кэшей
- лишнее чтение памяти
- разное представление мира у разных частей бота
Поэтому регистрация такого менеджера через RegisterSingleton<IEntityManager, EntityManager>() обычно очень логична.
Пример интерфейса может выглядеть так:
public interface IEntityManager
{
ImmutableArray<EntitySnapshot> EntitiesAround { get; }
EntitySnapshot? Target { get; }
void Refresh();
}
А в Script.csx это потом выглядит уже очень просто:
AddNewExtension<BotContainerExtensions>();
var entities = GetService<IEntityManager>();
var brain = GetService<IBotBrain>();
Log.Info($"Entities around: {entities.EntitiesAround.Length}");
brain.Tick();
После этого GetService<IEntityManager>() и GetService<IBotBrain>() начнут работать, потому что container extension уже зарегистрировала все нужные сервисы.
Что делает AddNewExtension<T>()
AddNewExtension<T>() говорит sandbox:
- найти extension нужного типа
- добавить ее в контейнер
- выполнить ее
Initialize() - зарегистрировать новые сервисы
Минимальный пример:
AddNewExtension<BotContainerExtensions>();
После этого можно получать сервисы, которые extension зарегистрировала:
var entities = GetService<IEntityManager>();
var brain = GetService<IBotBrain>();
Почему некоторые библиотеки просят явно вызвать AddNewExtension<T>()
Некоторые пакеты используют extensions как явный opt-in. То есть пакет подключен, но его сервисы не регистрируются, пока вы сами этого не попросите.
Так работают, например, некоторые модульные SDK.
ImGui
#r "nuget:EyeAuras.ImGuiSdk, 0.1.42"
using EyeAuras.ImGuiSdk;
AddNewExtension<ImGuiContainerExtensions>();
var imgui = GetService<IImGuiExperimentalApi>();
Frida
#r "nuget:EyeAuras.FridaSdk, 0.1.42"
using EyeAuras.FridaSdk;
AddNewExtension<FridaContainerExtensions>();
var frida = GetService<IFridaExperimentalApi>();
Это удобно, потому что вы подключаете только те подсистемы, которые реально нужны вашему скрипту.
Живые примеры:
AddNewExtension<T>() и Container.AddNewExtensionIfNotExists<T>() это не одно и то же
Похожи по названию, но используются в разных местах:
AddNewExtension<T>()вы вызываете изScript.csx, чтобы подключить extension к sandboxContainer.AddNewExtensionIfNotExists<T>()обычно вызывается внутри другой extension, если одна extension зависит от другой
То есть:
AddNewExtension<BotContainerExtensions>();
Это код уровня скрипта.
А вот это:
Container.AddNewExtensionIfNotExists<ImGuiContainerExtensions>();
Это уже код уровня DI-контейнера внутри самой extension.
Как runtime узнает про ScriptContainerExtension
EyeAuras сканирует загруженные сборки и ищет классы, которые наследуются от ScriptContainerExtension.
Также runtime во время setup скрипта умеет:
- анализировать свойства объекта скрипта
- заполнять свойства с
[Dependency] - заполнять свойства с
[Inject]
То есть extension отвечает за регистрацию сервисов, а runtime потом помогает использовать эти сервисы в самом скрипте.
[Inject], [Dependency] и GetService<T>() рядом с extensions
Эти механизмы хорошо работают вместе.
Но здесь есть важный практический нюанс:
- если сервис уже был зарегистрирован к моменту старта скрипта, его удобно получать через
[Inject]или[Dependency] - если вы прямо внутри
Script.csxсначала вызываетеAddNewExtension<T>(), а потом хотите получить новый сервис, самый безопасный и понятный путь этоGetService<T>()
Например, так корректно и понятно:
[Inject] public IAuraTreeScriptingApi AuraTree { get; init; } // built-in service
AddNewExtension<BotContainerExtensions>();
var entities = GetService<IEntityManager>();
Log.Info($"Entities around: {entities.EntitiesAround.Length}");
Практическое правило:
[Inject]и[Dependency]отлично подходят для уже доступных сервисов- после ручного
AddNewExtension<T>()чаще всего удобнее сразу делатьGetService<T>()
Важный нюанс: не new, а контейнер
Если класс зависит от других сервисов, лучше не писать:
var entityManager = new EntityManager(game);
Такой код иногда работает, но очень быстро превращается в ручную прокладку зависимостей.
Лучше так:
var entityManager = GetService<IEntityManager>();
Тогда DI сможет сам собрать объект и передать ему зависимости.
Более жизненный пример
Большая extension может не только регистрировать свои сервисы, но и подключать другие extensions.
Упрощенная идея выглядит так:
public sealed class BotContainerExtensions : ScriptContainerExtension
{
protected override void Initialize()
{
Container.AddNewExtensionIfNotExists<ImGuiContainerExtensions>();
Container.AddNewExtensionIfNotExists<FridaContainerExtensions>();
Container.AsServiceCollection().AddAiServices();
Container.RegisterSingleton<IEntityManager, EntityManager>();
Container.RegisterSingleton<IBotControllers, BotControllers>();
Container.RegisterType<IBotBrain, BotBrain>();
Container.RegisterType<IProfileManager, ProfileManager>();
Container.AsServiceCollection().AddBzGui();
}
}
То есть одна extension может стать "точкой входа" для большого набора библиотек, bot-сервисов и UI.
Когда это действительно стоит использовать
Container extensions особенно полезны, если:
- у вас уже несколько связанных классов
- вы хотите нормальную DI-сборку объектов
- вы пишете свой SDK или reusable helper-библиотеку
- вы делаете модульный UI или интеграцию с внешним движком
Если же у вас маленький скрипт на 20 строк, часто проще вообще ничего не усложнять и использовать обычный GetService<T>() прямо в Script.csx.
Как это связано с pack и mini-app
Для крупных решений container extensions особенно удобны, потому что помогают:
- держать код модульным
- переносить логику из одного файла в нормальные классы
- подключать SDK по требованию
- собирать один и тот же набор сервисов и в обычном скрипте, и в mini-app
Это хороший промежуточный слой между "маленький Script.csx" и "полноценное приложение".