Biker Quest Script

Biker Quest Script
bqs
Фрагмент кода на BQS
Языки программирования
Следующий язык SLThree
Разработка
Разработка с декабря 2021
Написан на C#
Платформы Windows
Язык программирования
Тип исполнения интерпретируемый
Типизация динамическая
Испытал влияние C#, Sunko
Повлиял на SLThree
Расширение .bqs
Статистика
Строк кода .cs: 21353

Biker Quest Script — C-подобный императивный скриптовый язык квестов (и не только) в Biker 3. Синтаксис очень близок к C#, что позволяет не учить целый ЯП.

  1. Навигация:
  2. Возможности и особенности
  3. Почему не Lua, Python и прочие
  4. Истоки разработки
  5. Возможности и фичи Biker Quest
  6. Спецификация
  7. Методы Biker 3
  8. Ссылки

Возможности и особенности

Ключевые:

Синтаксические:

Прочие:

Почему не Lua, Python и прочие

Ещё с Biker 2 у меня было желание разработать скриптовый язык, не порождая зависимости от уже существующих. Кроме того, собственный язык позволяет создавать свои абстракции и свой сахар. К тому же, BQS будучи привязанным к .NET, позволяет использовать методы Biker 3, которые можно писать непосредственно на C#, не плодя дополнительный код.

Истоки разработки

Имя подсветки этого языка — bqs2. Это неспроста так, ведь до этого существовал язык Biker Quest (.bikerquest), имевший расширенный синтаксис моего старого проекта под названием Sunko, который был очередной попыткой создать свой скриптовый язык. В определённой мере он справлялся со своей задачей, но был слаборасширяем.

  1. sunko
  2.     string x = {gettype {gettype {sunkoversion}}}
  3.     !write x
  4. end
Пример кода на Sunko

Biker Quest был похож на Sunko внутренним представлением и синтаксисом. Методы также, как и в Sunko, могли создаваться только вручную в коде и вызывались с помощью синтаксиса {MethodName arg1 arg2}. Это легко парсить. Но, понятное дело, писать на нём было крайне неудобно (лично я за два года существования этого языка так к нему и не привык).

  1. variant["0004", true] if ({money} >= QuestName.price)
  2.     setstage 2
  3.     setstage "QuestName" 2
  4.     REPUTATION = (REPUTATION + 1)
  5.     //declarations:
  6.     var x1: int64
  7.     var x2: int64 = start_date
  8.     var x3 = start_date
  9.     //assign:
  10.     x1 = 0
  11.     x2 += 1
  12.     x3 -= 1
  13.     if (true) then [setstage 1]
  14.     if (true) then [setstage 1] else [setstage 2]
  15.     if (true) then
  16.     begin
  17.     end
  18.     else
  19.     begin
  20.     end
  21.     while (true)
  22.     begin
  23.     end
  24.     begin
  25.     end
  26.     repeat until (true)
  27.     loop 100
  28.     end
  29. end. //Здесь end и end. это разные вещи
Код на Biker Quest
chooseОписания методов

Возможности и фичи Biker Quest (по сравнению с Sunko):

Паскалеподобный синтаксис смешанный с костылями для упрощения парсинга делал из этого языка этакого франкенштейна. Кроме того, синтаксис был захламлён кучей специальных ключевых слов вроде "setstage". Последней же каплей для меня стало то, что контекст pastetemplate было невозможно восстановить вообще и в конце декабря 2021 я решил полностью переписать Biker Quest. Так появился Biker Quest Script.

Спецификация

Подсветка сгенерирована с помощью рефлексии по BQS-конструкциям. Такой код подходит только для поверхностного ознакомления. Для подробного изучения скачайте RAW-версию кода.

  1. //Парсер языка однострочный. Одна строка - один оператор.
  2. //Комментарии только однострочные
  3. //У языка огромная схожесть с языком C#
  4. class ClassName
  5. {
  6.     //Классы условны. В языке нет ООП вообще. В языке даже структур нет!
  7.     //Но если очень хочется можно написать свою C#-библиотеку и подключить её
  8.     //Значение константы хранится в дереве.
  9.     const int maxprice = 300
  10.     //Значение переменной хранится в сохранении. При доступе к переменной вне контекста игры, она вернёт своё значение по умолчанию.
  11.     //переменные должны всегда быть сериализумого типа
  12.     //в качестве значения может использоваться вызов любых значений, для которых не нужен контекст игры
  13.     //в том числе вызов метода в том случае если для него не нужен контекст игры.
  14.     var int price = 200
  15.     //Список сериализуемых типов:
  16.     //sbyte, byte, short, ushort, int, uint, long, ulong, float, double, string, bool и byte[]
  17.     //Через последний тип можно представить что угодно
  18.     //Метод. Ключевое отличие от старого языка квестов. Строгости в обязательности возвращения значений нет.
  19.     //Возвращаемый тип может быть любой абсолютно. Внутренне void это object, имейте это ввиду.
  20.     void Method()
  21.     {
  22.         //Объявление переменной
  23.         //Точка с запятой в конце оператора не обязательна
  24.         var x = 0
  25.         //Также переменная объявляется при присваивании, если её не существует:
  26.         xx = 0
  27.         //Однако не стоит использовать эту возможность без необходимости.
  28.         {
  29.             //Объявленные переменные пропадают после конца области действия их объявления
  30.             //Если они заменили переменную, то она не вернётся назад!
  31.             var x = 0
  32.             var xx = 0
  33.         }
  34.         //Теперь обеих этих переменных не существует.
  35.         //Присваивание переменных:
  36.         x = 1
  37.         //Расширенное присваивание:
  38.         x += 1
  39.         x -= 1
  40.         x *= 1
  41.         x /= 1
  42.         x %= 1
  43.         x |= 1
  44.         x &= 1
  45.         x ^= 1
  46.         //Операций инкремента и декремента нет. Используйте += для ++x.
  47.         //Все операции являются утверждениеями, а значит их можно писать просто так:
  48.         1 + 2
  49.         //Каждая операция имеет свой приоритет (даже - над +). Имейте это ввиду.
  50.         //Порядок приоритета по возрастанию:
  51.         //"=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "&&", "||", "&", "|", "^",
  52.         //"is", "==", "!=", ">", ">=", "<", "<=", ">>", "<<", "+", "-", "*", "/", "%", "!", "~"
  53.         //Операции составляются из отдельных символов, так что a + = 1 страктуется как a += 1!
  54.         //Тернарная операция имеет минимальный приоритет
  55.         //Выражения имеют возвращаемый тип. Требуемы возвращаемый тип зависит от конструкции.
  56.         #Debug.Log(1)
  57.         var a1 = x + x
  58.         var b1 = 0.1 + a1
  59.         var c1 = (long )x
  60.         if (a1 is int )
  61.         {
  62.             var b = a is int a1
  63.             #Debug.Log(a is int )
  64.         }
  65.         //Старайтесь не переопределять существующие в контексте переменные. Если вы измените глубину их объявления, вы можете запутать самих себя.
  66.         //Операторы:
  67.         //Блок:
  68.         {
  69.         }
  70.         //Цикл while:
  71.         while (false)
  72.         {
  73.         }
  74.         //Или краткая версия:
  75.         while (false)null
  76.         //Цикл do-while:
  77.         do
  78.         {
  79.         }
  80.         while (false)
  81.         //Краткой версии не существует
  82.         //Цикл loop:
  83.         loop (1)
  84.         {
  85.         }
  86.         //Краткая версия:
  87.         loop (1)null
  88.         //Цикл for:
  89.         //for ([<Statement>]; [<Expression>]; [<Statement>]) [<Statement>];
  90.         for (var i = 0 ; i < 1 ; i += 1)
  91.         {
  92.             continue
  93.             break
  94.             for ( ; )null
  95.         }
  96.         //Условный оператор (краткие версии):
  97.         if (true)Log.Add(1)
  98.         //else if отдельная конструкция
  99.         else if (true)Log.Add(2)
  100.         else Log.Add(0)
  101.         //Несколько операторов на одной строке:
  102.         var a = 0 ; var b = 0
  103.         //Доступны во всех операторах (но недоступны как возвращающее значение краткого метода)
  104.         while (a + b < 4)a += 1 ; b += 1
  105.         //Интерполированные строки:
  106.         string s = "{a} {b}" ; #Debug.Log(s)
  107.         //Имена типа возвращают экземпляр System.Type
  108.         var t = string
  109.         //Обращаться к .NET-типам можно через имена, начинающиеся с ##
  110.         //Массивы и их инициализаторы
  111.         var a = new System.Type[1](string)
  112.         {
  113.             //Использование .NET-типов:
  114.             //#Type хранит полные имена типов в константах с которотким именем
  115.             //Ознакомьтесь со списком встроенных в Biker Quest Script методов
  116.             using #Type.ConsoleColor
  117.             //или: (первое будет быстрее)
  118.             using ##System.ConsoleColor
  119.             #Debug.Log(#ConsoleColor.Black)
  120.             //также можно
  121.             using ##System.Random as #R
  122.             //Вызов конструктора:
  123.             //Все конструкторы NET-типов именуются как #new
  124.             var r = #R.#new()
  125.             //Короткий вызов конструктора:
  126.             var r = #R.#new()
  127.             //Типы существуют до конца области действия, как и переменные.
  128.         }
  129.         //Чтобы можно было загружать типы из других сборок, необходимо загрузить эту .NET-сборку
  130.         #Script.LoadAssembly("KTXCore.dll")
  131.         var t = ""
  132.         //Цикл foreach:
  133.         foreach (var xin new object[](1, 2, 3, 4))t += "{x} "
  134.         //Лямбда-выражения и использование методов в качестве параметров
  135.         var m1 = => 1
  136.         var m2 = (x, y) => x() == y()
  137.         //Многострочная лямбда.
  138.         var m3 = =>
  139.         {
  140.             return m2(m1 , func_)
  141.         }
  142.         m3()
  143.         //Операция :: позволяет обратиться к экземплярному методу или свойству типа
  144.         m3 :: Invoke()
  145.         //Операция лямбды имеет самый низкий приоритет! Присвоение без var можно сделать только так:
  146.         m1 = ( => 1)
  147.         //(dynamize) для типов, отличных от object
  148.         var int_arr1 = (dynamize )new int[](1, 2, 3)
  149.         var t = int_arr1 :: Select(x => x * x) :: JoinIntoString(", ")
  150.         //Если у метода существуют перегрузки, то по имени нельзя получить его как метод.
  151.         //для этого есть специальный синтаксис
  152.         var lambda = (##method )#Math.Round(double , int )
  153.         var x = lambda(6.5 , 0)
  154.         //Захватывать контекст лямбды пока не умеют(
  155.         //Код можно выполнить здесь, но так чтобы он думал что он в другом дереве
  156.         //так он получит доступ ко всем именам другого дерева
  157.         #Script.Skill \ {var a = SportGroup.Level} \ {#Debug.Log(a)}
  158.          \ (##. )(s = 3) \ {#Linq.JoinIntoString(#Linq.Where(new object[](1, 2, 3, 4, 5) , x => x % s == 0) , " ")}
  159.     }
  160.     //Короткая запись метода:
  161.     //<TypeName> <FuncName>() => <Expression>;
  162.     //void <FuncName>() => <Statement>;
  163.     int func_() => 1
  164.     int property = 14
  165.     //Свойства
  166.     //Всего 2 вида свойств и 3 вида автосвойств
  167.     //Помните, что парсер однострочный, и точно следуйте синтаксису ниже
  168.     int Property1 => property
  169.     int Property2{
  170.         get => property
  171.         set => property = #value
  172.     }
  173.     int Property2_1{
  174.         get
  175.         {
  176.             return property
  177.         }
  178.         set
  179.         {
  180.             property = #value
  181.         }
  182.     }
  183.     //Автосвойства, как и переменные, могут быть только сериализуемых типов
  184.     int Property3{get ; set ; }
  185.     int Property4{get ; set ; } = 25
  186.     int Property5{get ; } = 30
  187. }
  188. quest QuestName
  189. {
  190.     //Возвращает строку типа (или группы) квестов для журнала заданий
  191.     const string #JournalGroup = "Второстепенные квесты"
  192.     //Доступ к словарю - Text(string key);
  193.     text
  194.     {
  195.         "desc1" = "Описание квеста в дневнике"
  196.         "desc2" = "Описание квеста появляющееся если выполнено условие"
  197.     }
  198.     //Краткая запись для небольших добавлений:
  199.     text "dddd" = ""
  200.     //Добавляет описание квесту в журнале
  201.     description
  202.     {
  203.         add "desc1"
  204.         add "desc2"if (true)
  205.     }
  206.     //Краткая запись для небольших добавлений:
  207.     description add "desc2"
  208.     description add "desc2"if (false)
  209.     //Возвращает true если в этом контексте можно начать печать квеста
  210.     //ui[<Place>] вернёт true только в случае совпадения с именем места. При любом другом значении вернёт false
  211.     //Удобно делать условия вроде a == 0 ? ui["All"] : ui["PlayerViewer"]
  212.     bool #UIStart(object ui) => ui["All"]
  213.     //Специальный метод. Вызывается при #Stage.NS (в состоянии неначатости квеста).
  214.     //Возвращает, можно ли начать выполнение квеста
  215.     bool #Available()
  216.     {
  217.         return true
  218.     }
  219.     //Специальный метод. Вызывается при первом запуске квеста если #Available вернуло true.
  220.     void #Start()
  221.     {
  222.         //Внутри каждого квеста на данный момент есть специальные локальные описания.
  223.         //Свойство (язык поддерживает .NET-свойства)
  224.         //свойство Stage возвращает стадию квеста из сохранения или задаёт эту стадию. Заданная стадия обязательно должна существовать
  225.         //иначе будет ошибка.
  226.         Stage = 1
  227.         //метод Text(string) возвращает значение по ключу словаря квеста
  228.         Text("desc1")
  229.     }
  230.     //Специальный метод. Определяет, будет ли пропускаться посимвольный вывод текста этого квеста.
  231.     bool #SkipCarriage() => false
  232.     //Специальный метод. Метод, который будет выполнен при пропуске квеста по кнопке Tab.
  233.     void #OnEscapeQuest() => null
  234.     //Специальный метод. Возвращает название квеста.
  235.     string #Title() => "Title name"
  236.     //Специальный метод. Возвращает описание квеста вдобавок к description
  237.     string #GetDescription() => ""
  238.     //Сокращённый способ определения названия квеста
  239.     title = "Title {(1+1)}"
  240.     stage [1]
  241.     {
  242.         //Специальные методы стадий и вариантов квестов недоступны для вызова!
  243.         //Возвращает доступность выполнения в этом месте UI
  244.         bool #UIStart(object ui) => ui["All"]
  245.         //
  246.         bool #Available() => true
  247.         //Специальный метод. Возвращает описание стадии квеста вдобавок к description
  248.         string #GetDescription() => ""
  249.         //Добавлять в словарь квеста можно и из стадий
  250.         text "1_d" = "Текст этой стадии"
  251.         text "1_d2" = "Текст этой стадии при выполнении условия"
  252.         description
  253.         {
  254.             add "1_d"
  255.             add "1_d2"if (true)
  256.         }
  257.         variant
  258.         {
  259.             //Специальный метод. Возвращающий текст варианта выбора
  260.             string #GetVariantText() => "Текст этого варианта"
  261.             //Специальный метод. Отвечает за действие при выборе этого варианта
  262.             void #Action() => return
  263.             //Специальный метод. Возвращает доступно ли выбрать этот вариант.
  264.             bool #Available() => false
  265.             //Специальный метод. Возвращает виден ли этот вариант в случае недоступности (будет показан серым)
  266.             bool #Visible() => false
  267.         }
  268.         //Сахарная сокращённая запись:
  269.         //variant => <Expression> => <Statement>[ => <Expression>[ => <Expression>]];
  270.         //порядок => #GetVariantText => #Action => #Available => #Visible
  271.         variant => "Текст этого варианта" => return => false => false
  272.     }
  273. }
RAW-версия спецификации

Хочу ещё добавить, что в спецификации описаны не все нюансы. К примеру, у перегрузки методов есть ограничения: будет выбран первый подходящий метод по порядку. То есть, если у нас есть, например, классы Item и BikeDetail : Item, и описать методы:

  1. void p(##Gameplay.Items.Item item) => return;
  2. void p(##Gameplay.Items.BikeDetails.BikeDetail bd) => return;
и попытаться вызвать p(bd) для BikeDetail, вызовется первый метод, так как он первее описан.

Ещё одно важное понятие — вызов экземплярных методов .NET-классов. В BQS нет объектов как таковых и все экземплярные методы могут быть вызваны только как статические с дополнительным первым аргументом. Например #Object.ToString(o); Если метод виртуальный и вы думаете какой конкретно класс использовать для вызова — используйте базовый.

Ссылки