Пару месяцев назад в разговоре мимоходом возник вопрос, возможно ли имитировать полноценный вызов процедур внутри Decorate, вместе с возвратом в произвольную точку входа. Возник -- и забылся, так как на чистом Decorate это всё-таки невозможно. Однако вчера я о нём вспомнил, и, так как сейчас практически у всей страны (или у мира?..) идут выходные, за пару дней написал свою реализацию -- на ZScript, но и для Decorate. И немного увлёкся, добавив невозможную параллельность.
Да, я знаю, что людей, пишущих на Decorate + ACS код под GZDoom, не так много -- однако они есть. Тем более что на ZScript библиотеку тоже вполне можно использовать.
* * *
Возможности: - Стандартный стек вызовов/стек возврата;
- Разбиение работы одного актора на множество потоков. На каждом из них независимо от других исполняется код (а также в реал-тайме могут подменяться и спрайты, и фреймы).
- Наследование и некоторый полиморфизм написанных процедур. С натяжкой можно сказать, что они идут с модификатором "virtual" (да, всё верно, в Decorate);
- Приостановка и прерывание работы определённого параллельного потока из-под любого другого.
В комплекте также идут несколько примеров использования. Работоспособность проверялась на GZDoom 2.4.0, GZDoom 3.3.0, QZDoom 2.1pre, LZDoom 3.87c, QZDoom g4.5.0, GZDoom 4.8.2.
Примечание: проект находится в бета-версии. Так как я на Decorate ничего не пишу, а в ZScript встроенным механизмом стейтов пользуюсь довольно редко, обновлять проект и править баги буду только по востребованию -- то есть, скорее всего, только тогда, когда кто-то сообщит о недоработке.
Версия, требующая минимум GZDoom 4.3.1. Фактически, не отличается ничем, кроме использования Mixin-конструкций. И в данном случае с ними писать даже правильнее, чем без них...
* * *
API и документация: Та же документация есть в файле README.md (правда, на английском), примеры использования внутри Decorate -- в директории "/Examples/".
Подпрограммы/Subroutines:
"SUBP_Call( State st )": сохранение текущего адреса и переход на указанную процедуру;
"SUBP_Return()": выталкивание из стека последнего сохранённого адреса, переход по нему.
Максимальное количество вложенных вызовов -- 255.
Параллелизация/Parallelizing:
"PAR_Thread( State startstate[, int steps[, int params]] )": создание нового параллельного потока, начинающего своё выполнение со стейта "startstate" (кстати, этот самый "startstate" также будет и его идентификатором).
║
╠ Аргумент "steps" определяет, сколько шагов (внимание: стейтов с ненулевой длительностью) параллельный поток имеет право существовать; после чего уничтожается, если его ещё где-то до этого не прервали. По умолчанию -- "PARSTEPS_Infinite", численно равно -1.
║
╠ "params" задаёт остальные параметры, в основном -- режим копирования спрайтов. Может быть одной из следующих констант:
╟-╖
║ ├ "PARSPR_All" или "PARSPR_Any". Поток будет копировать на актора все встречающиеся спрайты и фреймы, включая пустой "TNT1A0";
║ ├ "PARSPR_SkipTNT1", "PARSPR_SkipTNT1A0", "PARSPR_IgnoreTNT1" или "PARSPR_IgnoreTNT1A0" (является значением по умолчанию). То же самое, но "TNT1A0" игнорируется;
║ ├ "PARSPR_None", "PARSPR_IgnoreAll" или "PARSPR_SkipAll". Поток исключительно для исполнения функций, отключает работу со спрайтами.
║ ├ Флаг "PAREXEC_NoFunctionsCall" (через логическое ИЛИ, "|"). Блокирует вызовы всех функций (...включая "PAR_Stop()", ага).
"PAR_Stop( [State threadid] )": прерывает указанный поток. Если аргумент опущен, то прерывает тот, который его вызвал.
║
╠ Когда поток встречает инструкцию управления "Stop", "Goto Null", "Goto LightDone", уничтожается весь актор.
║
╚ Функция "PAR_Stop()" не может исполниться мгновенно, поэтому в данном случае обычно пишется задержка больше нуля: "TNT1 A 1 PAR_Stop".
"PAR_FrozeFlow( State threadid, bool froze )": приостанавливает или возобновляет работу указанного потока в зависимости от своего второго аргумента.
Звучит как что-то, открывающее принципиально новые возможности, но лично я от моддинга и кодинга далёк, поэтому слабо представляю реальные преимущества. Можно попросить привести пару выдуманных примеров реализации (взгляд в возможное будущее) в_противовес к текущему положению дел и ограничениям?
<...> примеры использования внутри Decorate -- в директории "/Examples/.
Внутри "SubprocTorches.dec" используется наследование функций. Там же проверял и аналог конструкции "override Smoke_subproc() { /* ... */ Super.Smoke_subproc(); }":
В "SubprocBaron.dec" -- прямолинейное объявление и использование пары процедур. Переписано с последовательности стейтов "See:" актора Abaddon из "Shut up and Bleed". По моему мнению, получилось читабельнее. Ну или как минимум структурнее. Оригинал тоже можно переписать на анонимных функциях, но с учётом того, что те же самые паттерны используются и в других местах актора, выйдет более громоздко. Здесь -- просто пример-копия из исходной модификации:
Скрытый текст:
Spawn:
TNT1 A 0 A_look
BRUS A 5 Bright A_wander
TNT1 A 0 A_look
BRUS A 5 Bright A_wander
TNT1 A 0 A_look
BRUS B 5 Bright A_wander
TNT1 A 0 A_look
BRUS B 5 Bright A_wander
TNT1 A 0 A_SpawnItem("HoofStep",0,0,0,0)
TNT1 A 0 A_SpawnItemEx("WalkFire",0,0,0,1,0,0,0,128)
TNT1 A 0 A_Quake(2, 4, 0, 512, "")
TNT1 A 0 A_look
BRUS C 5 Bright A_wander
TNT1 A 0 A_look
BRUS C 5 Bright A_wander
TNT1 A 0 A_look
BRUS D 5 Bright A_wander
TNT1 A 0 A_look
BRUS D 5 Bright A_wander
TNT1 A 0 A_SpawnItem("HoofStep",0,0,0,0)
TNT1 A 0 A_SpawnItemEx("WalkFire",0,0,0,1,0,0,0,128)
TNT1 A 0 A_Quake(2, 4, 0, 512, "")
Loop
"ParallelDoomImp.dec" демонстрирует использование как бы асинхронного исполнения сразу нескольких действий. Вот тут совершенно точно то же самое на чистом Decorate будет писаться... ну, несколько дольше.
А внутри "ParallelTechLamp.dec" все четыре состояния лампы имитируются с помощью двух потоков, сдвинутых друг относительно друга на четверть периода. Ну и плюс "учебные" эффекты -- например, запуск одного и того же потока с разным числом допустимых шагов.
Звучит как что-то, открывающее принципиально новые возможности <...>
Кстати, на это не ответил. Скорее, сильно облегчающее жизнь в некоторых местах: проект -- совершенно точно не панацея, хотя, действительно, может лечь на задачи идеально.
Наконец-то, через почти два с половиной месяца, поступило первое сообщение об ошибке -- и то, когда я сам посоветовал попробовать библиотеку. Вероятно, показатель, насколько часто у нас на форуме пользуются чем-то подобным.
Исправлено квазинеопределённое поведение при вызове функций стейтовых переходов ("A_Jump()" и подобные) внутри параллельных нитей; ссылка на сам коммит. YURA_111, спасибо!