Biker Quest Script
Biker Quest Script | |
---|---|
![]() | |
Фрагмент кода на BQS | |
Языки программирования | |
Следующий язык | SLThree |
Разработка | |
Разработка | с декабря 2021 |
Написан на | C# |
Платформы | Windows |
Язык программирования | |
Тип исполнения | интерпретируемый |
Типизация | динамическая |
Испытал влияние | C#, Sunko |
Повлиял на | SLThree |
Расширение | .bqs |
Статистика | |
Строк кода | .cs: 21353 |
Biker Quest Script — C-подобный императивный скриптовый язык квестов (и не только) в Biker 3. Синтаксис очень близок к C#, что позволяет не учить целый ЯП.
- Навигация:
- Возможности и особенности
- Почему не Lua, Python и прочие
- Истоки разработки
- Возможности и фичи Biker Quest
- Спецификация
- Методы Biker 3
- Ссылки
Возможности и особенности
Ключевые:
- Интерполированные строки
- Перегрузка методов
- Многочисленные квестовые абстракции
- Описание и вызов методов, которые могут принимать и возвращать любые типы
- Вездесущность контекста и тесная связь с рефлексией
- Названия типов возвращают System.Type этого типа
Синтаксические:
- Макросы и атрибуты
- Лямбда-выражения
- Свойства, в т.ч. автосвойства
- Сахарная конструкция variant =>
- Широкий набор операций, в том числе тернарная операция и расширенное присваивание
- Определение массивов через new и инициализатор массива
- Возможность явного приведения типов
- Циклы while, do-while, loop, for и foreach.
- Все операции являются утверждениями
- Каждое утверждение должно быть написано с новой строки
- Необязательная ; в конце утверждений
Прочие:
- Использование внешних .NET-библиотек. Это полезно, но в то же время опасно
- Использование любых .NET-типов и их методов через using
Почему не Lua, Python и прочие
Ещё с Biker 2 у меня было желание разработать скриптовый язык, не порождая зависимости от уже существующих. Кроме того, собственный язык позволяет создавать свои абстракции и свой сахар. К тому же, BQS будучи привязанным к .NET, позволяет использовать методы Biker 3, которые можно писать непосредственно на C#, не плодя дополнительный код.
Истоки разработки
Имя подсветки этого языка — bqs2. Это неспроста так, ведь до этого существовал язык Biker Quest (.bikerquest), имевший расширенный синтаксис моего старого проекта под названием Sunko, который был очередной попыткой создать свой скриптовый язык. В определённой мере он справлялся со своей задачей, но был слаборасширяем.
- sunko
- string x = {gettype {gettype {sunkoversion}}}
- !write x
- end
Biker Quest был похож на Sunko внутренним представлением и синтаксисом. Методы также, как и в Sunko, могли создаваться только вручную в коде и вызывались с помощью синтаксиса {MethodName arg1 arg2}. Это легко парсить. Но, понятное дело, писать на нём было крайне неудобно (лично я за два года существования этого языка так к нему и не привык).
- variant["0004", true] if ({money} >= QuestName.price)
- setstage 2
- setstage "QuestName" 2
- REPUTATION = (REPUTATION + 1)
- //declarations:
- var x1: int64
- var x2: int64 = start_date
- var x3 = start_date
- //assign:
- x1 = 0
- x2 += 1
- x3 -= 1
- if (true) then [setstage 1]
- if (true) then [setstage 1] else [setstage 2]
- if (true) then
- begin
- end
- else
- begin
- end
- while (true)
- begin
- end
- begin
- end
- repeat until (true)
- loop 100
- end
- end. //Здесь end и end. это разные вещи

Возможности и фичи Biker Quest (по сравнению с Sunko):
- Include-шаблоны кода (template)
- Слабочитаемый синтаксис квестовых абстракций (quest, text, stage, variant)
- Глобальные константы и переменные
- Однострочные условные конструкции
- Иной цикл for. Выглядел так: for [var x = 0] while (x < 10) do [x += 1]
- Возможность вызывать описанные вручную в дереве методы. Описывать их было нереальной болью.
Паскалеподобный синтаксис смешанный с костылями для упрощения парсинга делал из этого языка этакого франкенштейна. Кроме того, синтаксис был захламлён кучей специальных ключевых слов вроде "setstage". Последней же каплей для меня стало то, что контекст pastetemplate было невозможно восстановить вообще и в конце декабря 2021 я решил полностью переписать Biker Quest. Так появился Biker Quest Script.
Спецификация
Подсветка сгенерирована с помощью рефлексии по BQS-конструкциям. Такой код подходит только для поверхностного ознакомления. Для подробного изучения скачайте RAW-версию кода.
- //Парсер языка однострочный. Одна строка - один оператор.
- //Комментарии только однострочные
- //У языка огромная схожесть с языком C#
- class ClassName
- {
- //Классы условны. В языке нет ООП вообще. В языке даже структур нет!
- //Но если очень хочется можно написать свою C#-библиотеку и подключить её
- //Значение константы хранится в дереве.
- const int maxprice = 300
- //Значение переменной хранится в сохранении. При доступе к переменной вне контекста игры, она вернёт своё значение по умолчанию.
- //переменные должны всегда быть сериализумого типа
- //в качестве значения может использоваться вызов любых значений, для которых не нужен контекст игры
- //в том числе вызов метода в том случае если для него не нужен контекст игры.
- var int price = 200
- //Список сериализуемых типов:
- //sbyte, byte, short, ushort, int, uint, long, ulong, float, double, string, bool и byte[]
- //Через последний тип можно представить что угодно
- //Метод. Ключевое отличие от старого языка квестов. Строгости в обязательности возвращения значений нет.
- //Возвращаемый тип может быть любой абсолютно. Внутренне void это object, имейте это ввиду.
- void Method()
- {
- //Объявление переменной
- //Точка с запятой в конце оператора не обязательна
- var x = 0
- //Также переменная объявляется при присваивании, если её не существует:
- xx = 0
- //Однако не стоит использовать эту возможность без необходимости.
- {
- //Объявленные переменные пропадают после конца области действия их объявления
- //Если они заменили переменную, то она не вернётся назад!
- var x = 0
- var xx = 0
- }
- //Теперь обеих этих переменных не существует.
- //Присваивание переменных:
- x = 1
- //Расширенное присваивание:
- x += 1
- x -= 1
- x *= 1
- x /= 1
- x %= 1
- x |= 1
- x &= 1
- x ^= 1
- //Операций инкремента и декремента нет. Используйте += для ++x.
- //Все операции являются утверждениеями, а значит их можно писать просто так:
- 1 + 2
- //Каждая операция имеет свой приоритет (даже - над +). Имейте это ввиду.
- //Порядок приоритета по возрастанию:
- //"=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "&&", "||", "&", "|", "^",
- //"is", "==", "!=", ">", ">=", "<", "<=", ">>", "<<", "+", "-", "*", "/", "%", "!", "~"
- //Операции составляются из отдельных символов, так что a + = 1 страктуется как a += 1!
- //Тернарная операция имеет минимальный приоритет
- //Выражения имеют возвращаемый тип. Требуемы возвращаемый тип зависит от конструкции.
- #Debug.Log(1)
- var a1 = x + x
- var b1 = 0.1 + a1
- var c1 = (long )x
- if (a1 is int )
- {
- var b = a is int a1
- #Debug.Log(a is int )
- }
- //Старайтесь не переопределять существующие в контексте переменные. Если вы измените глубину их объявления, вы можете запутать самих себя.
- //Операторы:
- //Блок:
- {
- }
- //Цикл while:
- while (false)
- {
- }
- //Или краткая версия:
- while (false)null
- //Цикл do-while:
- do
- {
- }
- while (false)
- //Краткой версии не существует
- //Цикл loop:
- loop (1)
- {
- }
- //Краткая версия:
- loop (1)null
- //Цикл for:
- //for ([<Statement>]; [<Expression>]; [<Statement>]) [<Statement>];
- for (var i = 0 ; i < 1 ; i += 1)
- {
- continue
- break
- for ( ; )null
- }
- //Условный оператор (краткие версии):
- if (true)Log.Add(1)
- //else if отдельная конструкция
- else if (true)Log.Add(2)
- else Log.Add(0)
- //Несколько операторов на одной строке:
- var a = 0 ; var b = 0
- //Доступны во всех операторах (но недоступны как возвращающее значение краткого метода)
- while (a + b < 4)a += 1 ; b += 1
- //Интерполированные строки:
- string s = "{a} {b}" ; #Debug.Log(s)
- //Имена типа возвращают экземпляр System.Type
- var t = string
- //Обращаться к .NET-типам можно через имена, начинающиеся с ##
- //Массивы и их инициализаторы
- var a = new System.Type[1](string)
- {
- //Использование .NET-типов:
- //#Type хранит полные имена типов в константах с которотким именем
- //Ознакомьтесь со списком встроенных в Biker Quest Script методов
- using #Type.ConsoleColor
- //или: (первое будет быстрее)
- using ##System.ConsoleColor
- #Debug.Log(#ConsoleColor.Black)
- //также можно
- using ##System.Random as #R
- //Вызов конструктора:
- //Все конструкторы NET-типов именуются как #new
- var r = #R.#new()
- //Короткий вызов конструктора:
- var r = #R.#new()
- //Типы существуют до конца области действия, как и переменные.
- }
- //Чтобы можно было загружать типы из других сборок, необходимо загрузить эту .NET-сборку
- #Script.LoadAssembly("KTXCore.dll")
- var t = ""
- //Цикл foreach:
- foreach (var xin new object[](1, 2, 3, 4))t += "{x} "
- //Лямбда-выражения и использование методов в качестве параметров
- var m1 = => 1
- var m2 = (x, y) => x() == y()
- //Многострочная лямбда.
- var m3 = =>
- {
- return m2(m1 , func_)
- }
- m3()
- //Операция :: позволяет обратиться к экземплярному методу или свойству типа
- m3 :: Invoke()
- //Операция лямбды имеет самый низкий приоритет! Присвоение без var можно сделать только так:
- m1 = ( => 1)
- //(dynamize) для типов, отличных от object
- var int_arr1 = (dynamize )new int[](1, 2, 3)
- var t = int_arr1 :: Select(x => x * x) :: JoinIntoString(", ")
- //Если у метода существуют перегрузки, то по имени нельзя получить его как метод.
- //для этого есть специальный синтаксис
- var lambda = (##method )#Math.Round(double , int )
- var x = lambda(6.5 , 0)
- //Захватывать контекст лямбды пока не умеют(
- //Код можно выполнить здесь, но так чтобы он думал что он в другом дереве
- //так он получит доступ ко всем именам другого дерева
- #Script.Skill \ {var a = SportGroup.Level} \ {#Debug.Log(a)}
- \ (##. )(s = 3) \ {#Linq.JoinIntoString(#Linq.Where(new object[](1, 2, 3, 4, 5) , x => x % s == 0) , " ")}
- }
- //Короткая запись метода:
- //<TypeName> <FuncName>() => <Expression>;
- //void <FuncName>() => <Statement>;
- int func_() => 1
- int property = 14
- //Свойства
- //Всего 2 вида свойств и 3 вида автосвойств
- //Помните, что парсер однострочный, и точно следуйте синтаксису ниже
- int Property1 => property
- int Property2{
- get => property
- set => property = #value
- }
- int Property2_1{
- get
- {
- return property
- }
- set
- {
- property = #value
- }
- }
- //Автосвойства, как и переменные, могут быть только сериализуемых типов
- int Property3{get ; set ; }
- int Property4{get ; set ; } = 25
- int Property5{get ; } = 30
- }
- quest QuestName
- {
- //Возвращает строку типа (или группы) квестов для журнала заданий
- const string #JournalGroup = "Второстепенные квесты"
- //Доступ к словарю - Text(string key);
- text
- {
- "desc1" = "Описание квеста в дневнике"
- "desc2" = "Описание квеста появляющееся если выполнено условие"
- }
- //Краткая запись для небольших добавлений:
- text "dddd" = ""
- //Добавляет описание квесту в журнале
- description
- {
- add "desc1"
- add "desc2"if (true)
- }
- //Краткая запись для небольших добавлений:
- description add "desc2"
- description add "desc2"if (false)
- //Возвращает true если в этом контексте можно начать печать квеста
- //ui[<Place>] вернёт true только в случае совпадения с именем места. При любом другом значении вернёт false
- //Удобно делать условия вроде a == 0 ? ui["All"] : ui["PlayerViewer"]
- bool #UIStart(object ui) => ui["All"]
- //Специальный метод. Вызывается при #Stage.NS (в состоянии неначатости квеста).
- //Возвращает, можно ли начать выполнение квеста
- bool #Available()
- {
- return true
- }
- //Специальный метод. Вызывается при первом запуске квеста если #Available вернуло true.
- void #Start()
- {
- //Внутри каждого квеста на данный момент есть специальные локальные описания.
- //Свойство (язык поддерживает .NET-свойства)
- //свойство Stage возвращает стадию квеста из сохранения или задаёт эту стадию. Заданная стадия обязательно должна существовать
- //иначе будет ошибка.
- Stage = 1
- //метод Text(string) возвращает значение по ключу словаря квеста
- Text("desc1")
- }
- //Специальный метод. Определяет, будет ли пропускаться посимвольный вывод текста этого квеста.
- bool #SkipCarriage() => false
- //Специальный метод. Метод, который будет выполнен при пропуске квеста по кнопке Tab.
- void #OnEscapeQuest() => null
- //Специальный метод. Возвращает название квеста.
- string #Title() => "Title name"
- //Специальный метод. Возвращает описание квеста вдобавок к description
- string #GetDescription() => ""
- //Сокращённый способ определения названия квеста
- title = "Title {(1+1)}"
- stage [1]
- {
- //Специальные методы стадий и вариантов квестов недоступны для вызова!
- //Возвращает доступность выполнения в этом месте UI
- bool #UIStart(object ui) => ui["All"]
- //
- bool #Available() => true
- //Специальный метод. Возвращает описание стадии квеста вдобавок к description
- string #GetDescription() => ""
- //Добавлять в словарь квеста можно и из стадий
- text "1_d" = "Текст этой стадии"
- text "1_d2" = "Текст этой стадии при выполнении условия"
- description
- {
- add "1_d"
- add "1_d2"if (true)
- }
- variant
- {
- //Специальный метод. Возвращающий текст варианта выбора
- string #GetVariantText() => "Текст этого варианта"
- //Специальный метод. Отвечает за действие при выборе этого варианта
- void #Action() => return
- //Специальный метод. Возвращает доступно ли выбрать этот вариант.
- bool #Available() => false
- //Специальный метод. Возвращает виден ли этот вариант в случае недоступности (будет показан серым)
- bool #Visible() => false
- }
- //Сахарная сокращённая запись:
- //variant => <Expression> => <Statement>[ => <Expression>[ => <Expression>]];
- //порядок => #GetVariantText => #Action => #Available => #Visible
- variant => "Текст этого варианта" => return => false => false
- }
- }
Хочу ещё добавить, что в спецификации описаны не все нюансы. К примеру, у перегрузки методов есть ограничения: будет выбран первый подходящий метод по порядку. То есть, если у нас есть, например, классы Item и BikeDetail : Item, и описать методы:
- void p(##Gameplay.Items.Item item) => return;
- void p(##Gameplay.Items.BikeDetails.BikeDetail bd) => return;
Ещё одно важное понятие — вызов экземплярных методов .NET-классов. В BQS нет объектов как таковых и все экземплярные методы могут быть вызваны только как статические с дополнительным первым аргументом. Например #Object.ToString(o); Если метод виртуальный и вы думаете какой конкретно класс использовать для вызова — используйте базовый.