ScriptContainerExtension это способ зарегистрировать в DI-контейнере скрипта свои сервисы, helper-классы и API.
Если говорить совсем просто:
GetService<T>() получает уже зарегистрированный сервисScriptContainerExtension позволяет добавить новые регистрацииAddNewExtension<T>() это способ подключить такую extension во время работы скриптаЭта тема особенно полезна, если вы:
newУ EyeAuras уже есть sandbox и базовые сервисы. Но иногда вам хочется добавить в контейнер что-то свое:
IMyHelper -> MyHelperIBotState -> BotStateIFridaExperimentalApi -> FridaExperimentalApiIImGuiExperimentalApi -> ImGuiExperimentalApiДля этого и нужен ScriptContainerExtension.
Без него вы часто упретесь в один из двух сценариев:
newУ каждого скрипта есть sandbox и связанный с ним DI-контейнер.
Также внутри есть IScriptingApiContext. Это внутренний объект контекста, который хранит:
SolutionAnchors для ресурсов этого контекстаОбычно вы не создаете 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 принимает решения на основе уже готовых сервисовУпрощенный пример такой 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, потому что это единый источник правды для всего ботаIBotBrain часто удобнее регистрировать через RegisterType, если вы хотите создавать отдельный экземпляр под конкретную задачуIEntityManager это типичный singletonЕсли говорить игровыми терминами, EntityManager обычно отвечает за вещи вроде:
Такой сервис почти всегда хочется иметь один на весь бот, а не создавать заново в каждом 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:
Initialize()Минимальный пример:
AddNewExtension<BotContainerExtensions>();
После этого можно получать сервисы, которые extension зарегистрировала:
var entities = GetService<IEntityManager>();
var brain = GetService<IBotBrain>();
AddNewExtension<T>()Некоторые пакеты используют extensions как явный opt-in. То есть пакет подключен, но его сервисы не регистрируются, пока вы сами этого не попросите.
Так работают, например, некоторые модульные SDK.
#r "nuget:EyeAuras.ImGuiSdk, 0.1.42"
using EyeAuras.ImGuiSdk;
AddNewExtension<ImGuiContainerExtensions>();
var imgui = GetService<IImGuiExperimentalApi>();
#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.
ScriptContainerExtensionEyeAuras сканирует загруженные сборки и ищет классы, которые наследуются от 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 особенно полезны, если:
Если же у вас маленький скрипт на 20 строк, часто проще вообще ничего не усложнять и использовать обычный GetService<T>() прямо в Script.csx.
Для крупных решений container extensions особенно удобны, потому что помогают:
Это хороший промежуточный слой между "маленький Script.csx" и "полноценное приложение".