Скрипты EyeAuras это обычный C# код, который выполняется внутри готовой среды EyeAuras. Они подходят и для маленьких утилит на 20 строк, и для больших решений, которые позже можно вырастить в pack или mini-app.
Script.csx и обычные классыЭто один из самых важных моментов во всей системе.
Код в Script.csx проходит через специальную обработку EyeAuras:
Log, Sleep(...), GetService<T>(), AuraTree, Variables, cancellationTokenНапример, в самом Script.csx вы можете написать так:
Log.Info("Script started");
Sleep(100);
var input = GetService<ISendInputScriptingApi>();
input.KeyPress(Key.F);
А вот связанные классы, которые лежат рядом со скриптом, это уже самый обычный C#. У них нет "магического" прямого доступа к sandbox.
Если вы напишете отдельный класс, он не сможет просто так обратиться к Log или AuraTree:
public sealed class Helper
{
private readonly IFluentLog log;
public Helper(IFluentLog log)
{
this.log = log;
}
public void DoWork()
{
log.Info("Helper is working");
}
}
То есть правило простое:
Script.csx это специальная точка входа EyeAurasGetService<T>()Это важно помнить и людям, и AI: не весь код проекта имеет одинаковые "магические" возможности.
Самый важный ответ: скрипты нужны не потому, что "писать программу сложно", а потому, что EyeAuras уже является готовой платформой автоматизации.
Если писать отдельную программу с нуля, вам придется самостоятельно собрать вокруг вашей логики:
В EyeAuras большая часть этой инфраструктуры уже есть. Поэтому вы пишете только свою логику и сразу можете использовать готовые ауры, триггеры, переменные, CV API, ввод и UI.
Это дает несколько практических плюсов:
.sln, потом в pack, потом в mini-appЕсли же ваш проект вообще не использует возможности EyeAuras и по сути не зависит от ее среды выполнения, то обычная отдельная программа на .NET может быть более прямым вариантом.
С точки зрения языка это обычный C# код на .NET и Roslyn. С точки зрения продукта это код, который запускается внутри песочницы и получает доступ к API EyeAuras: логам, переменным, аурам, вводу, CV, UI и другим сервисам.
Подробнее: С чего начать, Песочница
Примеры:
Перед тем как писать отдельную программу, полезно понимать, что в EyeAuras уже есть готовые механизмы:
.slnЕсли вы делаете приватное или коммерческое решение, то licensing, key login, packs и защита кода часто экономят очень много времени.
Keybind и чем он похож на AutoHotkey[Keybind] это способ повесить обработчик клавиши прямо на метод скрипта.
Если вы раньше писали на AutoHotkey, то это очень похожая идея:
F1:: в AHK похоже на [Keybind("F1")]*F1:: похоже на [Keybind("F1", IgnoreModifiers = true)]~F1:: похоже на [Keybind("F1", SuppressKey = false)]d up:: похоже на [Keybind("D", ActivationType = KeybindActivationType.KeyUp)]Минимальные примеры:
[Keybind("F1")]
public void OnF1()
{
Log.Info("F1 pressed");
}
[Keybind("F1", IgnoreModifiers = true)]
public void OnF1WithAnyModifiers()
{
Log.Info("Works for F1, Shift+F1, Ctrl+F1");
}
[Keybind("D", ActivationType = KeybindActivationType.KeyUp)]
public void OnDReleased()
{
Log.Info("D released");
}
Зачем это нужно на практике:
Два важных нюанса:
Если вам нужно не пускать второй обработчик, пока не закончился первый, можно сделать так:
System.Threading.SemaphoreSlim hotkeyGate = new(1, 1);
[Keybind("F2")]
public void RunSingleHotkey()
{
if (!hotkeyGate.Wait(0))
{
Log.Info("Hotkey is already being processed");
return;
}
try
{
Log.Info("Handling F2");
Sleep(300);
}
finally
{
hotkeyGate.Release();
}
}
Небольшой полезный бонус: обработчик keybind тоже может принимать зависимости как параметры метода, если они уже доступны через DI.
Если же hotkey должен быть виден в дереве аур, сочетаться с условиями и жить как часть общей визуальной логики, часто удобнее использовать HotkeyIsActive trigger, а не [Keybind].
Подробнее: Горячие клавиши
Примеры:
Для пользовательского интерфейса в EyeAuras чаще всего есть два основных пути: Blazor Windows и ImGui.
ImGui обычно проще всего для старта.
Script.csxSleep(...) или тяжелый кодAddRenderer(...), а внутри render-метода только использовать уже готовое состояниеЭто хороший выбор, если вы хотите быстро сделать рабочий интерфейс и не заводить много файлов.
Blazor Windows обычно сложнее по структуре, но заметно богаче по визуальным возможностям.
Script.csx, *.razor, *.cs*.razor, а код в *.cs, потому что так проще читать, и сейчас там лучше ожидается IntelliSenseScript.csx обычно только создает окно или оверлей, а дальше состояние живет уже в самом компонентеScript.csx это скорее entry point создания окна, а не место, которое "рисует UI каждый кадр"OnInitializedAsync, а JavaScript, которому нужен уже существующий DOM, обычно в OnAfterFirstRenderAsyncПрактически это значит, что Blazor обычно требует чуть больше структуры, но и результат можно сделать намного "продуктовее".
Если очень коротко:
ImGui проще, быстрее и ближе к "все в одном файле"Blazor Windows сложнее, но красивее и гибче в оформленииЕще один важный момент:
Script.csx обычно выступает как entry point, а реальный UI живет в Razor-компонентахAddRenderer(...)Минимальный пример жизненного цикла в Blazor:
[Inject] public PoeShared.Blazor.Services.IJsPoeBlazorUtils PoeBlazorUtils { get; init; }
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
await PoeBlazorUtils.LoadCss("Styles.css");
}
protected override async Task OnAfterFirstRenderAsync()
{
await base.OnAfterFirstRenderAsync();
await PoeBlazorUtils.LoadScript("Script.js");
}
Подробнее: Blazor Windows, ImGui getting started
Примеры:
Script.csxЕсть несколько мелких особенностей, которые часто удивляют людей, потому что Script.csx компилируется не как обычный .cs-файл, а как Roslyn submission script.
using Namespace; и using для IDisposable это разные вещиОбычные директивы namespace работают как обычно:
using System.Text;
using OpenQA.Selenium;
А вот конструкции для освобождения ресурсов на верхнем уровне Script.csx требуют чуть больше аккуратности.
using для disposable лучше оборачивать в отдельный scopeЕсли вы хотите использовать освобождение ресурсов прямо на верхнем уровне Script.csx, лучше создать явный scope через {}.
Это касается и using var, и using (...) { ... }.
Простейший вариант:
{
using var canvas = GetService<IOnScreenCanvasScriptingApi>().Create();
Log.Info("Canvas created");
}
Или так:
{
using (var stream = GetService<IScriptFileProvider>().GetFileInfo("docs/help.md").CreateReadStream())
{
Log.Info("Stream opened");
}
}
Почему так:
Script.csx компилируется особым образомВнутри обычных методов, local functions, try, if, while и других блоков с using проблем обычно нет.
Практическое правило:
using Some.Namespace; в начале файла пишите как обычноusing var resource = ... на верхнем уровне Script.csx лучше помещать внутрь { ... }using (...) { ... } на верхнем уровне Script.csx тоже лучше помещать внутрь внешнего { ... }try { ... }, отдельный scope обычно не нужен, потому что scope у вас уже естьПримеры:
async/await можно использовать свободноВ Script.csx можно спокойно использовать async/await.
Например:
var text = await File.ReadAllTextAsync("input.txt");
Log.Info(text);
Или так:
async Task<int> LoadValue()
{
await Task.Delay(100);
return 42;
}
var value = await LoadValue();
Log.Info(value);
Практически это значит следующее:
await, компилятор сам адаптирует сигнатуруawaitTask<T>То есть в обычной жизни можно мыслить очень просто:
await — просто используйте awaitreturn значениеawait-ится, так что для автора скрипта это обычно выглядит очень естественноОтдельная рекомендация: async void лучше по возможности избегать и предпочитать async Task, даже если некоторые внутренние rewriter'ы EyeAuras умеют подправлять такие случаи автоматически.
Это еще одна частая точка путаницы.
Script.csx внутри EyeAuras не всегда ведет себя так, будто его полностью создают заново на каждый вызов. Если скрипт не перекомпилировался и его runtime-контекст остался тем же, часть верхнеуровневого состояния может переживать повторные запуски.
Практически это значит:
VariablesХорошее практическое правило:
VariablesПесочница это среда, в которой запускается ваш скрипт. Она:
Log, Sleep(...), GetService<T>()Если коротко, sandbox это тот самый host, который вам не нужно писать самостоятельно.
Подробнее: Песочница
Есть три основных варианта:
[Inject] + init: хороший выбор для основных зависимостей, которые задаются один раз при старте[Dependency] + set: когда зависимость должна быть свойством, которое при желании можно заменитьGetService<T>(): когда сервис нужен точечно и прямо сейчасМинимальные примеры:
[Inject] public IAuraTreeScriptingApi AuraTree { get; init; }
[Dependency] public ISendInputScriptingApi SendInput { get; set; }
var input = GetService<ISendInputScriptingApi>();
Если вы только начинаете, GetService<T>() обычно самый понятный. Для более крупного кода свойства с [Inject] или [Dependency] часто делают скрипт чище.
Подробнее: Dependency Injection, Script Container Extensions
Примеры:
cancellationToken и откуда он беретсяcancellationToken уже доступен в любом скрипте автоматически. EyeAuras сам пробрасывает его в sandbox перед запуском скрипта.
Если совсем коротко, CancellationToken в .NET это стандартный объект, через который программе можно аккуратно сказать коду: "пора останавливаться". Официальная документация: CancellationToken.
Практически это значит следующее:
Sleep(...) завершится раньшеВажно: часть cancellation-логики EyeAuras умеет добавлять автоматически, но в реальном коде лучше все равно писать явные и понятные cancellation-aware конструкции.
Есть два основных сценария.
Если вы уже создали оверлей, окно, renderer, подписки или другой фоновой объект, и дальше от скрипта не требуется активный polling, обычно достаточно:
cancellationToken.WaitHandle.WaitOne();
Это хороший вариант для:
Плюс этого подхода в том, что он не крутит пустой цикл и не тратит CPU без необходимости.
Если у вас активный цикл, используйте явную проверку отмены:
while (!cancellationToken.IsCancellationRequested)
{
DoSomething();
Sleep(50);
}
Это хороший вариант для:
Что важно:
Sleep(...), если на то нет очень веской причиныSleep(...) в EyeAuras уже умеет завершаться раньше при остановке скриптаЕсли коротко:
WaitOne() для сценария "все уже запущено, просто живем до stop"while (!cancellationToken.IsCancellationRequested) для сценария "пока живем, продолжаем работать"Примеры:
Sleep(...), а не Thread.Sleep(...)Для кода в Script.csx почти всегда лучше использовать именно Sleep(...) из sandbox.
Почему:
Sleep(...) связан с cancellationToken и умеет завершаться раньше при stopThread.Sleep(...) в Script.csx дополнительно переписывается rewriter'ом в Sleep(...)Подробнее: Переработка Sleep()
Ниже сравнительная картинка по точности разных подходов:

Практическое правило:
Script.csx пишите Sleep(...)Thread.Sleep(...) не используйтеTask.Delay(...) используйте только если вам действительно нужна именно асинхронная модель, и вы понимаете зачемScript.csxК Script.csx применяются code rewriters. Это еще одна причина, почему важно отделять код скрипта от обычных классов.
На практике полезно помнить как минимум про такие вещи:
while (true) EyeAuras пытается защитить, добавляя проверку cancellationTokenThread.Sleep(...) в коде скрипта заменяется на Sleep(...)CancellationToken, может быть автоматически подправлена runtimeЭто полезная страховка, но не повод писать небрежный код. Лучшая практика все равно такая:
while (!cancellationToken.IsCancellationRequested)Sleep(...)Anchors, ExecutionAnchors и зачем нужны disposable-ресурсыВ EyeAuras очень полезно мыслить не только "какой объект я создал", но и "когда он должен умереть".
Если объект надо потом корректно убрать, его обычно привязывают к CompositeDisposable. Официальная справка: CompositeDisposable.
Важные уровни жизни такие:
Anchors это якоря sandbox или объекта. Если добавить ресурс туда, он живет пока жив сам sandbox или сам объектExecutionAnchors это якоря текущего запуска скрипта. Stop/start скрипта их пересоздаетCompositeDisposable полезен внутри ваших helper-классов, когда вы сами собираете группу ресурсов в один "пакет"Короткое правило:
ExecutionAnchorsAnchorsПример:
var overlay = GetService<IImGuiExperimentalApi>()
.AddTo(ExecutionAnchors);
Это значит: overlay живет только пока жив текущий запуск скрипта.
Примеры:
IScriptingApiContextIScriptingApiContext это внутренний объект контекста скрипта. В нем лежат:
Solution текущего скриптаAnchors для ресурсов этого контекстаОбычно обычному автору скрипта не нужно создавать IScriptingApiContext вручную. Но полезно понимать идею:
AnchorsЭта тема особенно важна, если вы пишете свои библиотеки, extensions или выносите код из одного файла в более крупную структуру.
ExecutionAnchorsЕсли скрипт создает ресурсы, которые должны умереть вместе с текущим запуском скрипта, их стоит привязывать к ExecutionAnchors.
Типичные примеры:
Идея простая: если ресурс относится именно к текущему выполнению скрипта, привязывайте его к жизни этого выполнения.
Примеры:
На практике полезно различать три уровня:
Для большинства сценариев лучший API это ScriptVariable<T> через Variables.Get<T>(...).
var attempts = Variables.Get<int>("attempts");
attempts.Value++;
Преимущества такого подхода:
objectПодробнее: Переменные, IVariablesScriptingApi
Примеры:
У скрипта обычно есть доступ к текущей ауре и текущей папке:
AuraTree.AuraAuraTree.FolderТакже можно находить другие элементы по пути:
FindAuraByPath(...) / GetAuraByPath(...)FindFolderByPath(...) / GetFolderByPath(...)GetTriggerByPath<T>(...)Полезное правило:
Find* используйте, когда отсутствие объекта нормальноGet* используйте, когда отсутствие объекта это ошибкаВажно: у аур тоже есть свои переменные. То есть можно хранить данные не только на уровне текущего скрипта, но и на уровне ауры или папки.
var aura = AuraTree.GetAuraByPath(@".\Gameplay\Boss");
var phase = Variables.Get<string>(aura, "phase");
Подробнее: IAuraTreeScriptingApi, IAuraAccessor, Как найти ауру
Примеры:
AddNewExtension<T>() и когда он нуженНекоторые возможности EyeAuras оформлены как отдельные container extensions. Это способ подключить в DI-контейнер новый набор сервисов.
Например, некоторые библиотеки предполагают явное подключение прямо из Script.csx:
AddNewExtension<ImGuiContainerExtensions>();
var imgui = GetService<IImGuiExperimentalApi>();
AddNewExtension<FridaContainerExtensions>();
var frida = GetService<IFridaExperimentalApi>();
Это особенно полезно для модульных пакетов, которые не хочется тащить "всегда включенными".
Подробнее: Script Container Extensions, ImGui getting started
Примеры:
Если библиотека уже существует как NuGet-пакет, это обычно лучший вариант.
Почему:
pack EyeAuras заранее подтягивает NuGet-зависимости и включает их в packБазовый синтаксис:
#r "nuget: Some.Package, 1.2.3"
Минимальный пример:
#r "nuget: Newtonsoft.Json, 13.0.3"
using Newtonsoft.Json;
Log.Info(JsonConvert.SerializeObject(new { Hello = "World" }));
Важно: директивы #r пишутся именно в Script.csx. В обычных связанных *.cs-классах они не работают.
Подробнее: NuGet и сборки, упаковка NuGet в pack
Локальные сборки нужны, когда:
Лучший переносимый вариант обычно такой:
#r "assemblyPath: MyLib.dll"Минимальный пример:
#r "assemblyPath: MyLib.dll"
using MyLib;
var helper = new SomeLibraryHelper();
Log.Info(helper.ToString());
Это заметно лучше, чем абсолютный путь вроде D:\\Work\\Something\\MyLib.dll, потому что такой путь хорош только на машине автора.
Используйте абсолютные пути в основном для локальных экспериментов и отладки, а для переносимого решения предпочитайте:
Подробнее: Встроенные ресурсы, NuGet и сборки
Pack это portable-версия вашего решения на базе EyeAuras. Обычно она включает:
Что важно для скриптов:
Если скрипт распространяется дальше вашей машины, хорошее базовое правило такое:
NuGet > embedded DLL > абсолютный путь к DLLПодробнее: Packs, упаковка NuGet в pack, ресурсы в packs
Mini-app это уже не просто "скрипт в EyeAuras", а ваш продукт, построенный на базе EyeAuras.
Если коротко:
Mini-app хорош, когда вы хотите:
Да. Это один из главных плюсов всей системы.
Типовая лестница роста выглядит так:
.csx или C# Action.slnImport / Live Importpackmini-appТо есть EyeAuras-скрипт это не тупиковый "встроенный макрос", а нормальная точка входа в более крупный проект.
Подробнее: Интеграция с IDE, Mini-app
Если вы делаете свой продукт на базе EyeAuras, есть несколько связанных тем:
Для обычных локальных скриптов об этом можно вообще не думать. Для коммерческих или приватных решений это уже важная часть архитектуры.
Если задачу можно удобно собрать через готовые триггеры, ауры, переменные и действия, обычно так и стоит делать. Скрипт особенно хорош как "умный клей" между готовыми частями платформы.
Обычно проще и надежнее:
AuraTreeЧем сразу писать всю механику целиком вручную.
Даже если часть вещей EyeAuras умеет подстраховать автоматически, явный код обычно лучше:
Хорошие базовые шаблоны:
cancellationToken.WaitHandle.WaitOne();while (!cancellationToken.IsCancellationRequested) { ... }Script.csx используйте возможности sandbox, а в связанных классах пишите обычный C#Это очень простое, но очень полезное правило.
В Script.csx нормально писать:
Log.Info(...)Sleep(...)GetService<T>()AuraTree.Get...(...)А вот в helper-классах лучше либо:
GetService<T>(), чтобы контейнер сам собрал зависимостиЕсли решение потом будут запускать другие люди, избегайте:
D:\\...Для переносимого решения почти всегда лучше:
#r "nuget: ..."В старых примерах и документации еще можно встретить ISendInputUnstableScriptingApi, но для нового кода лучше ориентироваться на ISendInputScriptingApi.
Старый API полезно знать только потому, что он может встречаться в старых скриптах.
Unstable в названии API обычно не означает "сломанный" или "опасный". Обычно это значит только то, что интерфейс еще может меняться между версиями. Если стабильный аналог уже есть, предпочитайте его. Если нужная возможность пока есть только в Unstable, пользоваться ей нормально, просто учитывайте возможные breaking changes при обновлении.
Примеры с более современным API:
Variables.Get<T>()Работа через строковый индексатор возможна, но типизированный доступ через ScriptVariable<T> обычно заметно чище и безопаснее.
Find*, для обязательных Get*Это простое правило очень сильно снижает количество случайных падений и делает намерение кода понятнее.
ExecutionAnchorsЭто особенно важно для:
Так stop/start ведет себя предсказуемо, а за скриптом остается меньше мусора.
Примеры:
AddNewExtension<T>()Если пакет явно просит вызвать AddNewExtension<T>(), значит это не просто using, а реальная регистрация нового набора сервисов в контейнере.
Типичный пример:
AddNewExtension<ImGuiContainerExtensions>();
var imgui = GetService<IImGuiExperimentalApi>();
Без регистрации сервис может просто не появиться в контейнере.
Примеры:
.slnЕсли код стал большим, не пытайтесь бесконечно удерживать его в одном файле.
Хороший момент для перехода:
Для этого как раз и существует Export / Import / Live Import.
Практический нюанс: Export очищает целевую папку перед выгрузкой проекта, поэтому не экспортируйте в каталог, где лежат важные файлы.
Если вы регулярно пишете или поддерживаете большие скрипты, очень рекомендую сразу работать в связке IDE + AI + EyeAuras MCP. Мой личный выбор сейчас - Rider + AI Assistant/Codex + EyeAuras MCP, но Visual Studio и VS Code тоже вполне рабочие варианты.
Подробнее: Интеграция с IDE, AI и MCP
Для большинства задач самый простой и надежный путь это использовать уже настроенный Image Search trigger и из скрипта забрать его результат.
var trigger = AuraTree.GetTriggerByPath<IImageSearchTrigger>(@".\ImageSearch");
var result = trigger.Refresh();
if (!result.Detected.Success)
{
Log.Warn("Image not found");
return;
}
var screenRect = result.ToScreen(result.Detected.Bounds.Value);
Log.Info($"Image found @ {screenRect}");
Подробнее: Как найти изображение
Смотрите также:
Для этого обычно нужен ISendInputScriptingApi:
var input = GetService<ISendInputScriptingApi>();
input.KeyPress(Key.F);
input.MouseLeftClick();
В зависимости от задачи можно использовать:
KeyPress(...)KeyDown(...) / KeyUp(...)MouseMoveTo(...)MouseLeftClick() / MouseRightClick()Подробнее: ISendInputScriptingApi
Примеры:
Общий шаблон почти всегда одинаковый:
var input = GetService<ISendInputScriptingApi>();
var trigger = AuraTree.GetTriggerByPath<IImageSearchTrigger>(@".\ImageSearch");
var result = trigger.Refresh();
if (!result.Detected.Success)
{
return;
}
var screenRect = result.ToScreen(result.Detected.Bounds.Value);
input.MouseMoveTo(screenRect.Center());
input.MouseLeftClick();
Для текста логика такая же, только источником данных будет Text Search.
Подробнее: Как нажать на распознанное слово
Смотрите также:
Это одна из самых частых точек ошибок.
Обычно в EyeAuras встречаются три вида координат:
Правило безопасности простое:
ToScreen(...) или ToScreenPoint(...)Самые полезные методы:
ToScreen(rect)ToScreenPoint(rect)Center()Подробнее: WindowImageProcessedEventArgs
Примеры:
Embedded resources полезны, когда вы хотите "нести с собой" файлы вместе со скриптом:
Два самых простых сценария:
Показать картинку в Blazor:
<img src="Images/logo.png" />
Прочитать текстовый файл из скрипта:
var files = GetService<IScriptFileProvider>();
var helpText = files.ReadAllText("docs/help.md");
Log.Info(helpText);
Практическая мелочь, которая часто экономит время: лучше писать Images/logo.png или docs/help.md, а не просто logo.png или help.md. Короткие имена удобны, но при совпадении окончаний путей легко поймать не тот файл.
Если библиотека уже есть на NuGet, почти всегда лучше брать ее через #r "nuget: ...". Embedded resources особенно хороши для ваших собственных файлов и переносимых DLL.
Подробнее: Встроенные ресурсы
Примеры: