Обычный скрипт живёт недолго: запускается, выполняет задачу и завершается. Но иногда нужно хранить настройки и результаты дольше — между перезапусками скрипта и даже делиться ими с другими скриптами. Для этого в EyeAuras есть «внешние» переменные — на уровне скриптов, аур и папок.
Основные сценарии:
var x = 123;
int MyCounter { get; set; }
// ...
MyCounter = 16;
Log.Info($"Counter: {MyCounter}");
В «сыром» виде переменные можно читать и записывать через индексатор по строковому ключу:
Variables["myCounter"] = 1;
Log.Info($"Counter: {Variables["myCounter"]}");
Минусы: теряется тип, приходится вручную делать приведение. В C# это неудобно и ненадёжно.
Решение — ScriptVariable<T>: вы один раз задаёте имя и тип, а дальше работаете типобезопасно.
var myCounter = Variables.Get<int>("myCounter");
myCounter.Value = myCounter.Value + 1;
Log.Info($"Counter value: {myCounter}"); // 2, 3, 4, ...
Снаружи это выглядит как обычная переменная: её можно читать, писать и логировать. Но внутри значение хранится в общем «хранилище переменных», доступном и другим скриптам.
var v = Variables.Get<float>("speed");
v.Value = 1.5f;
var auraVar = Variables.Get<int>(@".\Gameplay\Bosses\Shaper", "attempts");
auraVar.Value++;
IHasVariables:var aura = AuraTree.GetAuraByPath(@".\Something");
var auraVar = Variables.Get<string>(aura, "status");
Log.Info($"Status: {auraVar.Value}");
ScriptVariable<T>Ниже — простые правила, которые лучше понять один раз, чем потом долго отлаживать.
Value
default(T) (0 для чисел, false для bool, null для ссылочных типов).var i = Variables.Get<int>("i");
Log.Info(i.Value); // 0, если переменная не задана
i.Value = 42; // записали 42
HasValue
true, если запись существует в хранилище (даже если для ссылочного типа там null).false, если записи нет вообще.var s = Variables.Get<string>("name");
Log.Info(s.HasValue); // false, записи нет
s.Value = null;
Log.Info(s.HasValue); // true, запись есть, но значение null
TryGetValue(out T value)
true, только если запись существует, значение НЕ null и его можно привести/конвертировать к T.false, если записи нет, значение null или тип не подходит.var f = Variables.Get<float>("ratio");
if (f.TryGetValue(out var val))
{
Log.Info($"ratio = {val}");
}
else
{
Log.Info("ratio is not set or null or cannot be converted to float");
}
GetOrThrow()
InvalidOperationException, если записи нет, значение null (для ссылочных типов) или его нельзя привести/конвертировать к T.var required = Variables.Get<int>("port");
var port = required.GetOrThrow(); // выбросит исключение, если нет записи, null или неверный тип
Remove()
true, если запись существовала, и false, если нет.Remove() чтение Value вернёт default(T), HasValue станет false, а Listen() получит событие «сброс к значению по умолчанию».var v = Variables.Get<int>("x").Set(10);
v.Remove(); // true
Log.Info(v.Value); // 0
Log.Info(v.HasValue); // false
Неявное преобразование к T
int i = myIntVar;var myIntVar = Variables.Get<int>("score").Set(5);
int i = myIntVar; // 5
var missing = Variables.Get<int>("missing");
int j = missing; // 0
Строковое представление
v.ToString() показывает имя и текущее значение, либо "<not set>", если записи не существует.var a = Variables.Get<int>("a");
Log.Info(a.ToString()); // "a = <not set>"
a.Value = 10;
Log.Info(a.ToString()); // "a = 10"
ScriptVariable<T> сначала пытается прочитать значение «как есть», а затем — сконвертировать его. Конвертация выполняется через внутренний репозиторий конвертеров (IBindingValueConverterRepository).
T — оно возвращается как есть.T — значение конвертируется и возвращается.T == string) подходит почти любой тип — будет возвращён value.ToString().Nullable<T> конвертация идёт к базовому типу.null, TryGetValue вернёт false, а Value — default(T).Value вернёт default(T), TryGetValue вернёт false, а GetOrThrow выбросит исключение.Примеры:
// 1) Типы совпадают — всё просто
Variables.Get<int>("n").Set(123);
Log.Info(Variables.Get<int>("n").Value); // 123
// 2) Чтение как другого типа
((IScriptVariable)Variables.Get<int>("mix")).Value = "abc"; // записали строку в обход типобезопасности
var v = Variables.Get<int>("mix");
Log.Info(v.Value); // 0 — default(int)
Log.Info(v.TryGetValue(out var x)); // false
// v.GetOrThrow(); // throws InvalidOperationException
// 3) Почти всё можно превратить в string
((IScriptVariable)Variables.Get<string>("text")).Value = 42;
Log.Info(Variables.Get<string>("text").Value); // "42"
Совет: если вам нужна конкретная конвертация, зарегистрируйте конвертер в IBindingValueConverterRepository. После этого TryGetValue и Value начнут возвращать нужный вам тип.
Listen()Если нужно реактивно отслеживать изменения переменной, используйте Listen().
IObservable<T>.default(T), если записи нет).DistinctUntilChanged).Remove() отправляет событие «сброса» к default(T).Пример:
var hp = Variables.Get<int>("hp");
var observed = new List<int>();
using var sub = hp.Listen().Subscribe(x => {
Log.Info($"HP: {x}");
observed.Add(x);
});
hp.Value = 100; // HP: 100
hp.Value = 100; // not emitted (distinct)
hp.Value = 90; // HP: 90
hp.Remove(); // HP: 0 (default)
Для ссылочных типов первым значением будет null, затем новые значения, а потом снова null при Remove() или если вы сами присвоите null.
Каждая аура и папка тоже является контейнером переменных. Здесь действует иерархия: дочерние элементы «видят» значения родителей и могут их переопределять.
Есть два способа доступа:
var auraVar = Variables.Get<int>(@".\Something", "myCounter");
auraVar.Value = 16;
var aura = AuraTree.GetAuraByPath(@".\Something");
var auraVar = Variables.Get<int>(aura, "myCounter");
auraVar.Value = 16;
Почему это важно: можно задать «значение по умолчанию» на папке — и все вложенные ауры смогут его читать. А там, где нужно, переопределить локально. Это очень удобно для общих настроек: целевое окно, input simulator и т. п.
Нюанс: переменные с префиксом default. часто используются как «базовые значения» по всему дереву. Например, если переопределить default.WindowSelector.TargetWindow, это повлияет на выбор окна во всех вложенных элементах.
Для удобства доступны операторы и неявные преобразования.
T:var score = Variables.Get<int>("score").Set(5);
int current = score; // 5
var a = Variables.Get<int>("a").Set(3);
var b = Variables.Get<int>("b").Set(3);
if (a == b) { /* same values */ }
if (a != b) { /* different values */ }
T — сравнимый числовой тип):var x = Variables.Get<int>("x").Set(2);
var y = Variables.Get<int>("y").Set(3);
if (y > x) { /* yes, 3 > 2 */ }
if (x < 3) { /* yes */ }
Пример «config -> worker»:
// Config script
Variables.Get<string>("target").Set("Shaper");
Variables.Get<int>("maxAttempts").Set(5);
// Worker script
var target = Variables.Get<string>("target").GetOrThrow();
var maxAttempts = Variables.Get<int>("maxAttempts").GetOrThrow();
for (var i = 0; i < maxAttempts; i++)
{
Log.Info($"Try {i+1} on {target}");
}
«Почему Value вернул 0/false/null, а не выбросил исключение?»
Value никогда не выбрасывает исключения. Если нужна строгая проверка, используйте GetOrThrow().«TryGetValue вернул false для null — это баг?»
TryGetValue null означает «значения нет». Для ссылочных типов HasValue при этом будет true (запись существует), а TryGetValue — false (потому что реального значения нет).«Почему Listen() не отправил то же значение второй раз?»
«Я удалил переменную, что отправит Listen()?»
default(T). Для int это 0, для string — null и так далее.ScriptVariable<T> один раз и переиспользуйте его.GetOrThrow().TryGetValue, а не сравнение с default(T).set null и Remove():
set null — запись остаётся (HasValue == true), но TryGetValue == false.Remove() — запись удаляется (HasValue == false), а Value возвращает default(T).Dispose() у возвращаемого IDisposable (обычно удобно через using var ...).snake_case или camelCase.var v = Variables.Get<int>("n");
// чтение/запись
int x = v.Value; // безопасно, default если нет значения/неверный тип
v.Value = 10; // запись
// строгий сценарий
var required = v.GetOrThrow(); // исключение, если нет значения/неверный тип/null
// аккуратное чтение
if (v.TryGetValue(out var val)) { /* ok */ } else { /* no value */ }
// подписка на изменения
using var sub = v.Listen().Subscribe(x => Log.Info($"n = {x}"));
// удаление
var removed = v.Remove();
// прочее
bool has = v.HasValue; // существует ли запись?
string name = v.Name;
IHasVariables src = v.Source; // откуда читает/куда пишет