ScriptContainerExtension is the way to register your own services, helper classes, and APIs in a script’s DI container.
Put simply:
GetService<T>() gives you a service that is already registeredScriptContainerExtension lets you add new registrationsAddNewExtension<T>() is how you connect such an extension at script runtimeThis is especially useful if you:
newEyeAuras already gives you a sandbox and a set of built-in services. But sometimes you want to add your own things to the container, for example:
IMyHelper -> MyHelperIBotState -> BotStateIFridaExperimentalApi -> FridaExperimentalApiIImGuiExperimentalApi -> ImGuiExperimentalApiThat is exactly what ScriptContainerExtension is for.
Without it, you usually run into one of two problems:
newEach script has its own sandbox and an associated DI container.
Inside that, there is also IScriptingApiContext. This is an internal context object that stores:
SolutionAnchors for resources in that contextNormally, you do not create IScriptingApiContext manually. But the idea is useful to understand: container extensions extend this service layer, and GetService<T>() works on top of it.
Script.csx and regular classesInside Script.csx, you can write things like this directly:
Log.Info("Hello");
var helper = GetService<MyHelper>();
But MyHelper is just a regular C# class. It does not have direct access to Log, AuraTree, GetService<T>(), or other sandbox features unless you pass them in yourself.
That is why container extensions are especially useful for related classes: they let DI create those classes and automatically inject their constructor dependencies.
In a real bot, an extension usually registers more than just a simple “counter” service.
A typical example looks like this:
IEntityManager stores all objects around the character, the current target, entity caches, and memory read resultsIGeodataManager handles geodata and navigationIBotBrain makes decisions based on ready-made servicesA simplified extension might look like this:
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>();
}
}
Why this is a good example:
IEntityManager makes sense as a Singleton, because it is the single source of truth for the whole botIBotBrain is often more convenient to register with RegisterType if you want a separate instance for a specific taskIEntityManager is a typical singletonIn game bot terms, EntityManager is usually responsible for things like:
In practice, you almost always want one instance for the entire bot, not a separate one in every helper class.
If you create multiple EntityManager instances, it is very easy to end up with:
That is why registering it with RegisterSingleton<IEntityManager, EntityManager>() is usually the most logical choice.
The interface might look like this:
public interface IEntityManager
{
ImmutableArray<EntitySnapshot> EntitiesAround { get; }
EntitySnapshot? Target { get; }
void Refresh();
}
And then in Script.csx, usage becomes very simple:
AddNewExtension<BotContainerExtensions>();
var entities = GetService<IEntityManager>();
var brain = GetService<IBotBrain>();
Log.Info($"Entities around: {entities.EntitiesAround.Length}");
brain.Tick();
After that, GetService<IEntityManager>() and GetService<IBotBrain>() start working because the container extension has already registered everything they need.
AddNewExtension<T>() doesAddNewExtension<T>() tells the sandbox to:
Initialize()Minimal example:
AddNewExtension<BotContainerExtensions>();
After that, you can resolve the services registered by that extension:
var entities = GetService<IEntityManager>();
var brain = GetService<IBotBrain>();
AddNewExtension<T>() explicitlySome packages use extensions as an explicit opt-in. In other words, the package is referenced, but its services are not registered until you ask for them.
That is how some modular SDKs work.
#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>();
This is convenient because you only enable the subsystems your script actually needs.
Live examples:
AddNewExtension<T>() and Container.AddNewExtensionIfNotExists<T>() are not the same thingThe names are similar, but they are used in different places:
AddNewExtension<T>() is called from Script.csx to connect an extension to the sandboxContainer.AddNewExtensionIfNotExists<T>() is usually called inside another extension when one extension depends on anotherSo this:
AddNewExtension<BotContainerExtensions>();
is script-level code.
And this:
Container.AddNewExtensionIfNotExists<ImGuiContainerExtensions>();
is DI-container-level code inside the extension itself.
ScriptContainerExtensionEyeAuras scans loaded assemblies and looks for classes that inherit from ScriptContainerExtension.
During script setup, the runtime can also:
[Dependency][Inject]So the extension is responsible for registering services, and the runtime helps you use those services inside the script.
[Inject], [Dependency], and GetService<T>() alongside extensionsThese mechanisms work well together.
But there is one practical detail to keep in mind:
[Inject] or [Dependency]AddNewExtension<T>() directly inside Script.csx and then want to get a newly registered service, the safest and clearest option is usually GetService<T>()For example, this is correct and easy to understand:
[Inject] public IAuraTreeScriptingApi AuraTree { get; init; } // built-in service
AddNewExtension<BotContainerExtensions>();
var entities = GetService<IEntityManager>();
Log.Info($"Entities around: {entities.EntitiesAround.Length}");
Practical rule of thumb:
[Inject] and [Dependency] are great for services that are already availableAddNewExtension<T>(), it is usually more convenient to call GetService<T>() right awaynewIf a class depends on other services, it is better not to write this:
var entityManager = new EntityManager(game);
Sometimes that works, but it quickly turns into manual dependency wiring.
This is better:
var entityManager = GetService<IEntityManager>();
Then DI can build the object for you and pass in its dependencies automatically.
A larger extension can do more than just register its own services. It can also connect other extensions.
The simplified idea looks like this:
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();
}
}
So one extension can become the main entry point for a large set of libraries, bot services, and UI.
Container extensions are especially useful if:
If you only have a tiny 20-line script, it is often simpler not to overcomplicate things and just use GetService<T>() directly in Script.csx.
For larger solutions, container extensions are especially convenient because they help you:
This is a good middle layer between a small Script.csx and a full application.